1. 程式人生 > >React整體流程

React整體流程

文章地址:http://react-china.org/t/react-redux/9072

react的元件化

react的一個元件很明顯的由dom檢視和state資料組成,兩個部分涇渭分明。state是資料中心,它的狀態決定著檢視的狀態。這時候發現似乎和我們一直推崇的MVC開發模式有點區別,沒了Controller控制器,那使用者互動怎麼處理,資料變化誰來管理?然而這並不是react所要關心的事情,它只負責ui的渲染。與其他框架監聽資料動態改變dom不同,react採用setState來控制檢視的更新。setState會自動呼叫render函式,觸發檢視的重新渲染,如果僅僅只是state資料的變化而沒有呼叫setState,並不會觸發更新。 元件就是擁有獨立功能的檢視模組,許多小的元件組成一個大的元件,整個頁面就是由一個個元件組合而成。它的好處是利於重複利用和維護。


react的 Diff演算法

react的diff演算法用在什麼地方呢?當元件更新的時候,react會建立一個新的虛擬dom樹並且會和之前儲存的dom樹進行比較,這個比較多過程就用到了diff演算法,所以元件初始化的時候是用不到的。react提出了一種假設,相同的元件具有類似的結構,而不同的元件具有不同的結構。在這種假設之上進行逐層的比較,如果發現對應的節點是不同的,那就直接刪除舊的節點以及它所包含的所有子節點然後替換成新的節點。如果是相同的節點,則只進行屬性的更改。
對於列表的diff演算法稍有不同,因為列表通常具有相同的結構,在對列表節點進行刪除,插入,排序的時候,單個節點的整體操作遠比一個個對比一個個替換要好得多,所以在建立列表的時候需要設定key值,這樣react才能分清誰是誰。當然不寫key值也可以,但這樣通常會報出警告,通知我們加上key值以提高react的效能。


react元件是怎麼來的

元件的創造方法為React.createClass() ——創造一個類,react系統內部設計了一套類系統,利用它來創造react元件。但這並不是必須的,我們還可以用es6的class類來創造元件,這也是Facebook官方推薦的寫法。

這兩種寫法實現的功能一樣但是原理卻是不同,es6的class類可以看作是建構函式的一個語法糖,可以把它當成建構函式來看,extends實現了類之間的繼承 —— 定義一個類Main 繼承React.Component所有的屬性和方法,元件的生命週期函式就是從這來的。constructor是構造器,在例項化物件時呼叫,super呼叫了父類的constructor創造了父類的例項物件this,然後用子類的建構函式進行修改。這和es5的原型繼承是不同的,原型繼承是先創造一個例項化物件this,然後再繼承父級的原型方法。瞭解了這些之後我們在看元件的時候就清楚很多。

當我們使用元件< Main />時,其實是對Main類的例項化——new Main,只不過react對這個過程進行了封裝,讓它看起來更像是標籤。

有三點值得注意:1、定義類名字的首字母必須大寫 2、因為class變成了關鍵字,類選擇器需要用className來代替。 3、類和模組內部預設使用嚴格模式,所以不需要用use strict指定執行模式。


元件的生命週期

元件在初始化時會觸發5個鉤子函式:

1、getDefaultProps()

設定預設的props,也可以用dufaultProps設定元件的預設屬性。

2、getInitialState()

在使用es6的class語法時是沒有這個鉤子函式的,可以直接在constructor中定義this.state。此時可以訪問this.props。

3、componentWillMount()

元件初始化時只調用,以後元件更新不呼叫,整個生命週期只調用一次,此時可以修改state。

4、 render()

react最重要的步驟,建立虛擬dom,進行diff演算法,更新dom樹都在此進行。此時就不能更改state了。

5、componentDidMount()

元件渲染之後呼叫,可以通過this.getDOMNode()獲取和操作dom節點,只調用一次。

在更新時也會觸發5個鉤子函式:

6、componentWillReceivePorps(nextProps)

元件初始化時不呼叫,元件接受新的props時呼叫。

7、shouldComponentUpdate(nextProps, nextState)

react效能優化非常重要的一環。元件接受新的state或者props時呼叫,我們可以設定在此對比前後兩個props和state是否相同,如果相同則返回false阻止更新,因為相同的屬性狀態一定會生成相同的dom樹,這樣就不需要創造新的dom樹和舊的dom樹進行diff演算法對比,節省大量效能,尤其是在dom結構複雜的時候。不過呼叫this.forceUpdate會跳過此步驟。

8、componentWillUpdata(nextProps, nextState)

元件初始化時不呼叫,只有在元件將要更新時才呼叫,此時可以修改state

9、render()

不多說

10、componentDidUpdate()

元件初始化時不呼叫,元件更新完成後呼叫,此時可以獲取dom節點。

還有一個解除安裝鉤子函式

11、componentWillUnmount()

元件將要解除安裝時呼叫,一些事件監聽和定時器需要在此時清除。

以上可以看出來react總共有10個周期函式(render重複一次),這個10個函式可以滿足我們所有對元件操作的需求,利用的好可以提高開發效率和元件效能。


react-router路由

Router和Route就是React的一個元件,它並不會被渲染,只是一個建立內部路由規則的配置物件,根據匹配的路由地址展現相應的元件。Route則對路由地址和元件進行繫結,Route具有巢狀功能,表示路由地址的包涵關係,這和元件之間的巢狀並沒有直接聯絡。Route可以向繫結的元件傳遞7個屬性:children,history,location,params,route,routeParams,routes,每個屬性都包涵路由的相關的資訊。比較常用的有children(以路由的包涵關係為區分的元件),location(包括地址,引數,地址切換方式,key值,hash值)。react-router提供Link標籤,這只是對a標籤的封裝,值得注意的是,點選連結進行的跳轉並不是預設的方式,react-router阻止了a標籤的預設行為並用pushState進行hash值的轉變。切換頁面的過程是在點選Link標籤或者後退前進按鈕時,會先發生url地址的轉變,Router監聽到地址的改變根據Route的path屬性匹配到對應的元件,將state值改成對應的元件並呼叫setState觸發render函式重新渲染dom。

當頁面比較多時,專案就會變得越來越大,尤其對於單頁面應用來說,初次渲染的速度就會很慢,這時候就需要按需載入,只有切換到頁面的時候才去載入對應的js檔案。react配合webpack進行按需載入的方法很簡單,Route的component改為getComponent,元件用require.ensure的方式獲取,並在webpack中配置chunkFilename。

const chooseProducts = (location, cb) => {
require.ensure([], require => {
    cb(null, require('../Component/chooseProducts').default)
},'chooseProducts')
}

const helpCenter = (location, cb) => {
require.ensure([], require => {
    cb(null, require('../Component/helpCenter').default)
},'helpCenter')
}

const saleRecord = (location, cb) => {
require.ensure([], require => {
    cb(null, require('../Component/saleRecord').default)
},'saleRecord')
}

const RouteConfig = (
<Router history={history}>
    <Route path="/" component={Roots}>
        <IndexRoute component={index} />//首頁
        <Route path="index" component={index} />
        <Route path="helpCenter" getComponent={helpCenter} />//幫助中心
        <Route path="saleRecord" getComponent={saleRecord} />//銷售記錄
        <Redirect from='*' to='/'  />
    </Route>
</Router>
);


元件之間的通訊

react推崇的是單向資料流,自上而下進行資料的傳遞,但是由下而上或者不在一條資料流上的元件之間的通訊就會變的複雜。解決通訊問題的方法很多,如果只是父子級關係,父級可以將一個回撥函式當作屬性傳遞給子級,子級可以直接呼叫函式從而和父級通訊。

元件層級巢狀到比較深,可以使用上下文Context來傳遞資訊,這樣在不需要將函式一層層往下傳,任何一層的子級都可以通過this.context直接訪問。

兄弟關係的元件之間無法直接通訊,它們只能利用同一層的上級作為中轉站。而如果兄弟元件都是最高層的元件,為了能夠讓它們進行通訊,必須在它們外層再套一層元件,這個外層的元件起著儲存資料,傳遞資訊的作用,這其實就是redux所做的事情。

元件之間的資訊還可以通過全域性事件來傳遞。不同頁面可以通過引數傳遞資料,下個頁面可以用location.query來獲取。


先說說redux:

redux主要由三部分組成:store,reducer,action。

store是一個物件,它有四個主要的方法:

1、dispatch:

用於action的分發——在createStore中可以用middleware中介軟體對dispatch進行改造,比如當action傳入dispatch會立即觸發reducer,有些時候我們不希望它立即觸發,而是等待非同步操作完成之後再觸發,這時候用redux-thunk對dispatch進行改造,以前只能傳入一個物件,改造完成後可以傳入一個函式,在這個函式裡我們手動dispatch一個action物件,這個過程是可控的,就實現了非同步。

2、subscribe:

監聽state的變化——這個函式在store呼叫dispatch時會註冊一個listener監聽state變化,當我們需要知道state是否變化時可以呼叫,它返回一個函式,呼叫這個返回的函式可以登出監聽。
let unsubscribe = store.subscribe(() => {console.log('state發生了變化')})

3、getState:

獲取store中的state——當我們用action觸發reducer改變了state時,需要再拿到新的state裡的資料,畢竟資料才是我們想要的。getState主要在兩個地方需要用到,一是在dispatch拿到action後store需要用它來獲取state裡的資料,並把這個資料傳給reducer,這個過程是自動執行的,二是在我們利用subscribe監聽到state發生變化後呼叫它來獲取新的state資料,如果做到這一步,說明我們已經成功了。

4、replaceReducer:

替換reducer,改變state修改的邏輯。

store可以通過createStore()方法建立,接受三個引數,經過combineReducers合併的reducer和state的初始狀態以及改變dispatch的中介軟體,後兩個引數並不是必須的。store的主要作用是將action和reducer聯絡起來並改變state。

action是一個物件,其中type屬性是必須的,同時可以傳入一些資料。action可以用actionCreactor進行創造。dispatch就是把action物件傳送出去。

reducer是一個函式,它接受一個state和一個action,根據action的type返回一個新的state。根據業務邏輯可以分為很多個reducer,然後通過combineReducers將它們合併,state樹中有很多物件,每個state物件對應一個reducer,state物件的名字可以在合併時定義。

像這個樣子:

const reducer = combineReducers({
     a: doSomethingWithA,
     b: processB,
     c: c
})

combineReducers其實也是一個reducer,它接受整個state和一個action,然後將整個state拆分發送給對應的reducer進行處理,所有的reducer會收到相同的action,不過它們會根據action的type進行判斷,有這個type就進行處理然後返回新的state,沒有就返回預設值,然後這些分散的state又會整合在一起返回一個新的state樹。

接下來分析一下整體的流程,首先呼叫store.dispatch將action作為引數傳入,同時用getState獲取當前的狀態樹state並註冊subscribe的listener監聽state變化,再呼叫combineReducers並將獲取的state和action傳入。combineReducers會將傳入的state和action傳給所有reducer,reducer會根據state的key值獲取與自己對應的state,並根據action的type返回新的state,觸發state樹的更新,我們呼叫subscribe監聽到state發生變化後用getState獲取新的state資料。

redux的state和react的state兩者完全沒有關係,除了名字一樣。

上面分析了redux的主要功能,那麼react-redux到底做了什麼?


react-redux

如果只使用redux,那麼流程是這樣的:

component --> dispatch(action) --> reducer --> subscribe --> getState --> component

用了react-redux之後流程是這樣的:

component --> actionCreator(data) --> reducer --> component

store的三大功能:dispatch,subscribe,getState都不需要手動來寫了。react-redux幫我們做了這些,同時它提供了兩個好基友Provider和connect。

Provider是一個元件,它接受store作為props,然後通過context往下傳,這樣react中任何元件都可以通過contex獲取store。也就意味著我們可以在任何一個元件裡利用dispatch(action)來觸發reducer改變state,並用subscribe監聽state的變化,然後用getState獲取變化後的值。但是並不推薦這樣做,它會讓資料流變的混亂,過度的耦合也會影響元件的複用,維護起來也更麻煩。

connect --connect(mapStateToProps, mapDispatchToProps, mergeProps, options)是一個函式,它接受四個引數並且再返回一個函式--wrapWithConnect,wrapWithConnect接受一個元件作為引數wrapWithConnect(component),它內部定義一個新元件Connect(容器元件)並將傳入的元件(ui元件)作為Connect的子元件然後return出去。

所以它的完整寫法是這樣的:connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)

mapStateToProps(state, [ownProps]):

mapStateToProps 接受兩個引數,store的state和自定義的props,並返回一個新的物件,這個物件會作為props的一部分傳入ui元件。我們可以根據元件所需要的資料自定義返回一個物件。ownProps的變化也會觸發mapStateToProps

function mapStateToProps(state) {
   return { todos: state.todos };
}

mapDispatchToProps(dispatch, [ownProps]):

mapDispatchToProps如果是物件,那麼會和store繫結作為props的一部分傳入ui元件。如果是個函式,它接受兩個引數,bindActionCreators會將action和dispatch繫結並返回一個物件,這個物件會和ownProps一起作為props的一部分傳入ui元件。所以不論mapDispatchToProps是物件還是函式,它最終都會返回一個物件,如果是函式,這個物件的key值是可以自定義的

function mapDispatchToProps(dispatch) {
   return {
      todoActions: bindActionCreators(todoActionCreators, dispatch),
      counterActions: bindActionCreators(counterActionCreators, dispatch)
   };
}

mapDispatchToProps返回的物件其屬性其實就是一個個actionCreator,因為已經和dispatch繫結,所以當呼叫actionCreator時會立即傳送action,而不用手動dispatch。ownProps的變化也會觸發mapDispatchToProps。

mergeProps(stateProps, dispatchProps, ownProps):

將mapStateToProps() 與 mapDispatchToProps()返回的物件和元件自身的props合併成新的props並傳入元件。預設返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。

options:

pure = true 表示Connect容器元件將在shouldComponentUpdate中對store的state和ownProps進行淺對比,判斷是否發生變化,優化效能。為false則不對比。

其實connect並沒有做什麼,大部分的邏輯都是在它返回的wrapWithConnect函式內實現的,確切的說是在wrapWithConnect內定義的Connect元件裡實現的。

下面是一個完整的 react --> redux --> react 流程:

一、Provider元件接受redux的store作為props,然後通過context往下傳。

二、connect函式在初始化的時候會將mapDispatchToProps物件繫結到store,如果mapDispatchToProps是函式則在Connect元件獲得store後,根據傳入的store.dispatch和action通過bindActionCreators進行繫結,再將返回的物件繫結到store,connect函式會返回一個wrapWithConnect函式,同時wrapWithConnect會被呼叫且傳入一個ui元件,wrapWithConnect內部使用class Connect extends Component定義了一個Connect元件,傳入的ui元件就是Connect的子元件,然後Connect元件會通過context獲得store,並通過store.getState獲得完整的state物件,將state傳入mapStateToProps返回stateProps物件、mapDispatchToProps物件或mapDispatchToProps函式會返回一個dispatchProps物件,stateProps、dispatchProps以及Connect元件的props三者通過Object.assign(),或者mergeProps合併為props傳入ui元件。然後在ComponentDidMount中呼叫store.subscribe,註冊了一個回撥函式handleChange監聽state的變化。

三、此時ui元件就可以在props中找到actionCreator,當我們呼叫actionCreator時會自動呼叫dispatch,在dispatch中會呼叫getState獲取整個state,同時註冊一個listener監聽state的變化,store將獲得的state和action傳給combineReducers,combineReducers會將state依據state的key值分別傳給子reducer,並將action傳給全部子reducer,reducer會被依次執行進行action.type的判斷,如果有則返回一個新的state,如果沒有則返回預設。combineReducers再次將子reducer返回的單個state進行合併成一個新的完整的state。此時state發生了變化。Connect元件中呼叫的subscribe會監聽到state發生了變化,然後呼叫handleChange函式,handleChange函式內部首先呼叫getState獲取新的state值並對新舊兩個state進行淺對比,如果相同直接return,如果不同則呼叫mapStateToProps獲取stateProps並將新舊兩個stateProps進行淺對比,如果相同,直接return結束,不進行後續操作。如果不相同則呼叫this.setState()觸發Connect元件的更新,傳入ui元件,觸發ui元件的更新,此時ui元件獲得新的props,react --> redux --> react 的一次流程結束。

上面的有點複雜,簡化版的流程是:

一、Provider元件接受redux的store作為props,然後通過context往下傳。

二、connect函式收到Provider傳出的store,然後接受三個引數mapStateToProps,mapDispatchToProps和元件,並將state和actionCreator以props傳入元件,這時元件就可以呼叫actionCreator函式來觸發reducer函式返回新的state,connect監聽到state變化呼叫setState更新元件並將新的state傳入元件。

connect可以寫的非常簡潔,mapStateToProps,mapDispatchToProps只不過是傳入的回撥函式,connect函式在必要的時候會呼叫它們,名字不是固定的,甚至可以不寫名字。

簡化版本:connect(state => state, action)(Component);


專案搭建

上面說了react,react-router和redux的知識點。但是怎麼樣將它們整合起來,搭建一個完整的專案。

1、先引用 react.js,redux,react-router 等基本檔案,建議用npm安裝,直接在檔案中引用。

2、從 react.js,redux,react-router 中引入所需要的物件和方法。

import React, {Component, PropTypes} from 'react';
import ReactDOM, {render} from 'react-dom';
import {Provider, connect} from 'react-redux';
import {createStore, combineReducers, applyMiddleware} from 'redux';
import { Router, Route, Redirect, IndexRoute, browserHistory, hashHistory } from 'react-router';

3、根據需求建立頂層ui元件,每個頂層ui元件對應一個頁面。

4、建立actionCreators和reducers,並用combineReducers將所有的reducer合併成一個大的reduer。利用createStore建立store並引入combineReducers和applyMiddleware。

5、利用connect將actionCreator,reuder和頂層的ui元件進行關聯並返回一個新的元件。

6、利用connect返回的新的元件配合react-router進行路由的部署,返回一個路由元件Router。

7、將Router放入最頂層元件Provider,引入store作為Provider的屬性。

8、呼叫render渲染Provider元件且放入頁面的標籤中。

可以看到頂層的ui元件其實被套了四層元件,Provider,Router,Route,Connect,這四個並不會在檢視上進行任何改變,它們只是功能性的。

通常我們在頂層的ui元件列印props時可以看到一堆屬性:

上圖的頂層ui元件屬性總共有18個,如果剛剛接觸react,可能對這些屬性怎麼來的感到困惑,其實這些屬性來自五個地方:

元件自定義屬性1個,actionCreator返回的物件6個,reducer返回的state4個,Connect元件屬性0個,以及Router注入的屬性7個。

總結react中遇到的坑和一些小的知識點

在使用react 中經常會遇到各種個樣的問題,如果對react不熟悉則會對遇到的問題感到莫名其妙而束手無策,接下來分析一下react中容易遇到的問題和注意點。

1、setState()是非同步的
this.setState()會呼叫render方法,但並不會立即改變state的值,state是在render方法中賦值的。所以執行this.setState()後立即獲取state的值是不變的。同樣的直接賦值state並不會出發更新,因為沒有呼叫render函式。

2、元件的生命週期
componentWillMount,componentDidMount 只有在初始化的時候才呼叫。
componentWillReceivePorps,shouldComponentUpdate,componentWillUpdata,componentDidUpdate 只有元件在更新的時候才被呼叫,初始化時不呼叫。

3、reducer必須返回一個新的物件才能出發元件的更新
因為在connect函式中會對新舊兩個state進行淺對比,如果state只是值改變但是引用地址沒有改變,connect會認為它們相同而不觸發更新。

4、無論reducer返回的state是否改變,subscribe中註冊的所有回撥函式都會被觸發。

5、元件命名的首字母必須是大寫,這是類命名的規範。

6、元件解除安裝之前,加在dom元素上的監聽事件,和定時器需要手動清除,因為這些並不在react的控制範圍內,必須手動清除。

7、按需載入時如果元件是通過export default 暴露出去,那麼require.ensure時必須加上default。

require.ensure([], require => {
    cb(null, require('../Component/saleRecord').default)
},'saleRecord')

8、react的路由有hashHistory和browserHistory,hashHistory由hash#控制跳轉,一般用於正式線上部署,browserHistory就是普通的地址跳轉,一般用於開發階段。

9、標籤裡用到的,for 要寫成htmlFor,因為for已經成了關鍵字。

10、componentWillUpdate中可以直接改變state的值,而不能用setState。

11、如果使用es6class類繼承react的component元件,constructor中必須呼叫super,因為子類需要用super繼承component的this,否則例項化的時候會報錯。