先附上項目源碼地址和原文章地址:https://github.com/bailicangd...javascript
react的功能其實很單一,主要負責渲染的功能,現有的框架,好比angular是一個大而全的框架,用了angular幾乎就不須要用其餘工具輔助配合,可是react不同,他只負責ui渲染,想要作好一個項目,每每須要其餘庫和工具的配合,好比用redux來管理數據,react-router管理路由,react已經全面擁抱es6,因此es6也得掌握,webpack就算是不會配置也要會用,要想提升性能,須要按需加載,immutable.js也得用上,還有單元測試。。。。java
用腳本進行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,並不會觸發更新。 組件就是擁有獨立功能的視圖模塊,許多小的組件組成一個大的組件,整個頁面就是由一個個組件組合而成。它的好處是利於重複利用和維護。webpack
react的diff算法用在什麼地方呢?當組件更新的時候,react會建立一個新的虛擬dom樹而且會和以前儲存的dom樹進行比較,這個比較多過程就用到了diff算法,因此組件初始化的時候是用不到的。react提出了一種假設,相同的節點具備相似的結構,而不一樣的節點具備不一樣的結構。在這種假設之上進行逐層的比較,若是發現對應的節點是不一樣的,那就直接刪除舊的節點以及它所包含的全部子節點而後替換成新的節點。若是是相同的節點,則只進行屬性的更改。git
對於列表的diff算法稍有不一樣,由於列表一般具備相同的結構,在對列表節點進行刪除,插入,排序的時候,單個節點的總體操做遠比一個個對比一個個替換要好得多,因此在建立列表的時候須要設置key值,這樣react才能分清誰是誰。固然不寫key值也能夠,但這樣一般會報出警告,通知咱們加上key值以提升react的性能。es6
組件的創造方法爲React.createClass() ——創造一個類,react系統內部設計了一套類系統,利用它來創造react組件。但這並非必須的,咱們還能夠用es6的class類來創造組件,這也是Facebook官方推薦的寫法。github
這兩種寫法實現的功能同樣可是原理倒是不一樣,es6的class類能夠看做是構造函數的一個語法糖,能夠把它當成構造函數來看,extends實現了類之間的繼承 —— 定義一個類Main 繼承React.Component全部的屬性和方法,組件的生命週期函數就是從這來的。constructor是構造器,在實例化對象時調用,super調用了父類的constructor創造了父類的實例對象this,而後用子類的構造函數進行修改。這和es5的原型繼承是不一樣的,原型繼承是先創造一個實例化對象this,而後再繼承父級的原型方法。瞭解了這些以後咱們在看組件的時候就清楚不少。web
當咱們使用組件< Main />時,實際上是對Main類的實例化——new Main,只不過react對這個過程進行了封裝,讓它看起來更像是一個標籤。算法
有三點值得注意:一、定義類名字的首字母必須大寫 二、由於class變成了關鍵字,類選擇器須要用className來代替。 三、類和模塊內部默認使用嚴格模式,因此不須要用use strict指定運行模式。npm
組件在初始化時會觸發5個鉤子函數:
一、getDefaultProps()
設置默認的props,也能夠用dufaultProps設置組件的默認屬性。
二、getInitialState()
在使用es6的class語法時是沒有這個鉤子函數的,能夠直接在constructor中定義this.state。此時能夠訪問this.props。
三、componentWillMount()
組件初始化時只調用,之後組件更新不調用,整個生命週期只調用一次,此時能夠修改state。
四、 render()
react最重要的步驟,建立虛擬dom,進行diff算法,更新dom樹都在此進行。此時就不能更改state了。
五、componentDidMount()
組件渲染以後調用,能夠經過this.getDOMNode()獲取和操做dom節點,只調用一次。
在更新時也會觸發5個鉤子函數:
六、componentWillReceivePorps(nextProps)
組件初始化時不調用,組件接受新的props時調用。
七、shouldComponentUpdate(nextProps, nextState)
react性能優化很是重要的一環。組件接受新的state或者props時調用,咱們能夠設置在此對比先後兩個props和state是否相同,若是相同則返回false阻止更新,由於相同的屬性狀態必定會生成相同的dom樹,這樣就不須要創造新的dom樹和舊的dom樹進行diff算法對比,節省大量性能,尤爲是在dom結構複雜的時候。不過調用this.forceUpdate會跳過此步驟。
八、componentWillUpdata(nextProps, nextState)
組件初始化時不調用,只有在組件將要更新時才調用,此時能夠修改state
九、render()
很少說
十、componentDidUpdate()
組件初始化時不調用,組件更新完成後調用,此時能夠獲取dom節點。
還有一個卸載鉤子函數
十一、componentWillUnmount()
組件將要卸載時調用,一些事件監聽和定時器須要在此時清除。
以上能夠看出來react總共有10個周期函數(render重複一次),這個10個函數能夠知足咱們全部對組件操做的需求,利用的好能夠提升開發效率和組件性能。
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並非必須的,它的做用至關於在頂層組件之上又加了一個組件,做用是進行邏輯運算、儲存數據和實現組件尤爲是頂層組件的通訊。若是組件之間的交流很少,邏輯不復雜,只是單純的進行視圖的渲染,這時候用回調,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讓它們聯繫在一塊兒。
明顯比第一張要複雜,其實兩張圖說的是同一件事。從上而下慢慢分析:
store是一個對象,它有四個主要的方法:
一、dispatch:
用於action的分發——在createStore中能夠用middleware中間件對dispatch進行改造,好比當action傳入dispatch會當即觸發reducer,有些時候咱們不但願它當即觸發,而是等待異步操做完成以後再觸發,這時候用redux-thunk對dispatch進行改造,之前只能傳入一個對象,改造完成後能夠傳入一個函數,在這個函數裏咱們手動dispatch一個action對象,這個過程是可控的,就實現了異步。
二、subscribe:
監聽state的變化——這個函數在store調用dispatch時會註冊一個listener監聽state變化,當咱們須要知道state是否變化時能夠調用,它返回一個函數,調用這個返回的函數能夠註銷監聽。
let unsubscribe = store.subscribe(() => {console.log('state發生了變化')})
三、getState:
獲取store中的state——當咱們用action觸發reducer改變了state時,須要再拿到新的state裏的數據,畢竟數據纔是咱們想要的。getState主要在兩個地方須要用到,一是在dispatch拿到action後store須要用它來獲取state裏的數據,並把這個數據傳給reducer,這個過程是自動執行的,二是在咱們利用subscribe監聽到state發生變化後調用它來獲取新的state數據,若是作到這一步,說明咱們已經成功了。
四、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到底作了什麼?
若是隻使用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組件裏實現的。
1、Provider組件接受redux的store做爲props,而後經過context往下傳。
2、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的變化。
3、此時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 的一次流程結束。
上面的有點複雜,簡化版的流程是:
1、Provider組件接受redux的store做爲props,而後經過context往下傳。
2、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的知識點。可是怎麼樣將它們整合起來,搭建一個完整的項目。
一、先引用 react.js,redux,react-router 等基本文件,建議用npm安裝,直接在文件中引用。
二、從 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';
三、根據需求建立頂層ui組件,每一個頂層ui組件對應一個頁面。
四、建立actionCreators和reducers,並用combineReducers將全部的reducer合併成一個大的reduer。利用createStore建立store並引入combineReducers和applyMiddleware。
五、利用connect將actionCreator,reuder和頂層的ui組件進行關聯並返回一個新的組件。
六、利用connect返回的新的組件配合react-router進行路由的部署,返回一個路由組件Router。
七、將Router放入最頂層組件Provider,引入store做爲Provider的屬性。
八、調用render渲染Provider組件且放入頁面的標籤中。
能夠看到頂層的ui組件其實被套了四層組件,Provider,Router,Route,Connect,這四個組件並不會在視圖上改變react,它們只是功能性的。
一般咱們在頂層的ui組件打印props時能夠看到一堆屬性:
上圖的頂層ui組件屬性總共有18個,若是剛剛接觸react,可能對這些屬性怎麼來的感到困惑,其實這些屬性來自五個地方:
組件自定義屬性1個,actionCreator返回的對象6個,reducer返回的state4個,Connect組件屬性0個,以及Router注入的屬性7個。