分享一個 react + redux 完整的項目,同時寫一下個人感悟
做React需要會什麼?
react的功能其實很單一,主要負責渲染的功能,現有的框架,比如angular是一個大而全的框架,用了angular幾乎就不需要用其他工具輔助配合,但是react不一樣,他只負責ui渲染,想要做好一個項目,往往需要其他庫和工具的配合,比如用redux來管理數據,react-router管理路由,react已經全面擁抱es6,所以es6也得掌握,webpack就算是不會配置也要會用,要想提高性能,需要按需載入,immutable.js也得用上,還有單元測試。。。。
React 是什麼
用腳本進行DOM操作的代價很昂貴。有個貼切的比喻,把DOM和JavaScript各自想像為一個島嶼,它們之間用收費橋樑連接,js每次訪問DOM,都要途徑這座橋,並交納「過橋費」,訪問DOM的次數越多,費用也就越高。 因此,推薦的做法是盡量減少過橋的次數,努力待在ECMAScript島上。因為這個原因react的虛擬dom就顯得難能可貴了,它創造了虛擬dom並且將它們儲存起來,每當狀態發生變化的時候就會創造新的虛擬節點和以前的進行對比,讓變化的部分進行渲染。整個過程沒有對dom進行獲取和操作,只有一個渲染的過程,所以react說是一個ui框架。
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就是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推崇的是單向數據流,自上而下進行數據的傳遞,但是由下而上或者不在一條數據流上的組件之間的通信就會變的複雜。解決通信問題的方法很多,如果只是父子級關係,父級可以將一個回調函數當作屬性傳遞給子級,子級可以直接調用函數從而和父級通信。
組件層級嵌套到比較深,可以使用上下文getChildContext來傳遞信息,這樣在不需要將函數一層層往下傳,任何一層的子級都可以通過this.context直接訪問。
兄弟關係的組件之間無法直接通信,它們只能利用同一層的上級作為中轉站。而如果兄弟組件都是最高層的組件,為了能夠讓它們進行通信,必須在它們外層再套一層組件,這個外層的組件起著保存數據,傳遞信息的作用,這其實就是redux所做的事情。
組件之間的信息還可以通過全局事件來傳遞。不同頁面可以通過參數傳遞數據,下個頁面可以用location.param來獲取。其實react本身很簡單,難的在於如何優雅高效的實現組件之間數據的交流。
Redux
首先,redux並不是必須的,它的作用相當於在頂層組件之上又加了一個組件,作用是進行邏輯運算、儲存數據和實現組件尤其是頂層組件的通信。如果組件之間的交流不多,邏輯不複雜,只是單純的進行視圖的渲染,這時候用回調,context就行,沒必要用redux,用了反而影響開發速度。但是如果組件交流特別頻繁,邏輯很複雜,那redux的優勢就特別明顯了。我第一次做react項目的時候並沒有用redux,所有的邏輯都是在組件內部實現,當時為了實現一個邏輯比較複雜的購物車,洋洋洒洒居然寫了800多行代碼,回頭一看我自己都不知道寫的是啥,畫面太感人。
先簡單說一下redux和react是怎麼配合的。react-redux提供了connect和Provider兩個好基友,它們一個將組件與redux關聯起來,一個將store傳給組件。組件通過dispatch發出action,store根據action的type屬性調用對應的reducer並傳入state和這個action,reducer對state進行處理並返回一個新的state放入store,connect監聽到store發生變化,調用setState更新組件,此時組件的props也就跟著變化。
流程是這個樣子的:
值得注意的是connect,Provider,mapStateToProps,mapDispatchToProps是react-redux提供的,redux本身和react沒有半毛錢關係,它只是數據處理中心,沒有和react產生任何耦合,是react-redux讓它們聯繫在一起。
接下來具體分析一下,redux以及react-redux到底是怎麼實現的。
先上一張圖
明顯比第一張要複雜,其實兩張圖說的是同一件事。從上而下慢慢分析:
先說說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:**
>action是一個對象,其中type屬性是必須的,同時可以傳入一些數據。action可以用actionCreactor進行創造。dispatch就是把action對象發送出去。
**reducer:**
>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值是可以自定義的
unction 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,這四個組件並不會在視圖上改變react,它們只是功能性的。
通常我們在頂層的ui組件列印props時可以看到一堆屬性:
上圖的頂層ui組件屬性總共有18個,如果剛剛接觸react,可能對這些屬性怎麼來的感到困惑,其實這些屬性來自五個地方:
組件自定義屬性1個,actionCreator返回的對象6個,reducer返回的state4個,Connect組件屬性0個,以及Router注入的屬性7個。
項目源碼地址:https://github.com/bailicangdu/pxq
推薦閱讀:
※不一樣的 vue 實戰 (3): 布局與組件
※React.js: web開發者的14個工具和資源
※前後端分離部署,腳手架環境下開發的前端代碼部署流程是怎樣的?
※Vue2技術棧歸納與精粹
※react中使用echarts就這麼簡單