redux原始碼解讀(簡單易懂版)
寫這篇文章是因為我所有能搜尋到的文章都太!復!雜!了!,一上來就做了個todo list,並且使用了一大堆react-redux已經封裝好的方法,所有的一切對我來說都是黑盒的,並且藕合度非常低,我根本不知道為什麼這樣寫最後就會那樣,有時候甚至這樣寫根本不能得到那樣的結果,然而由於我並不知道中間發生了什麼,所以只能去網上搜到底哪裡出了錯,往往還搜不到解決方案!!所以這裡寫了個最簡單的例子,並且一步一步從原始寫法到封裝寫法,以便理解封裝那些方法的作用。
先搭建最簡單的redux
redux的作用無需多言,因為react只負責view的部分,而不管理元件間的互動和時間傳遞,redux就是來解決這一問題的。
redux組成部分
建立一個redux需要先定義一些靜態的東西,就好比我們現在要用樂高搭個變形金剛,先把你要的形狀的樂高準備好。1.資料 2.動作 3.處理資料用的方法
1 . 建立初始state(資料)
state裡你可以放任何你需要的資料,他就是個普通物件
const initialState={ name:'test', count:1 } 複製程式碼
2 . 建立actions (動作)
action描述要發起的動作型別,以及完成這個動作需要的引數,所以type是action必要的屬性,其他的屬性就隨意定義你需要的,這裡我定義了三個動作,加(addCount),減(minusCount),改變name(changeName)
//描述動作型別的常量 const ADD_COUNT='ADD_COUNT'; const MINUS_COUNT='MINUS_COUNT'; const CHANGE_NAME='CHANGE_NAME'; //actions //加動作 function addCount(count){ return { type:ADD_COUNT, count: count } } //減動作 function minusCount(count){ return { type:MINUS_COUNT, count:count } } //改變name function changeName(name){ return { type:'CHANGE_NAME', name:name } } 複製程式碼
3 . reducer (處理資料用的方法)
reducer處理action動作並返回新的state
//reducer function countReducer(state = initialState, action) { switch (action.type) { case ADD_COUNT: return Object.assign({},state,{count:state.count+action.count}); case MINUS_COUNT: return Object.assign({},state,{count:state.count-action.count}); case CHANGE_NAME return Object.assign({},state,{name:action.name}); default: return Object.assign({},state); } } 複製程式碼
現在,所有的樂高都準備好了,我們可以“組裝”了。
4 .store(組裝)
redux提供一個createStore方法,利用我們剛定義的state和reducer來生成一個store
import { createStore}from 'redux'; let store= createStore(countReducer,initialState); 複製程式碼
store提供四個方法dispach,subscribe,getState,replaceReducer 分別用來觸發動作,訂閱動作,獲取當前的state,更換reducer。
5 . 結合react
react可以看成redux的使用者,他需要用redux來發起事件,訂閱事件。
下面定義了兩個元件App 和 Count。其中Count是App的子元件,在render中把store作為props傳遞給App,使得App可以使用store提供的方法.
import {render} from 'react-dom'; import{Component} from 'react'; class Count extends Component{ constructor(props){ super(props); } render(){ return( <div > Hello, Im a count: {this.props.count} </div> ); } } class App extends Component{ constructor(props){ super(props); this.state=this.props.store.getState(); } componentDidMount(){ let _this = this; let store=this.props.store //訂閱store發起的所有事件,獲取新的state用來更新自身的state store.subscribe(function(){ _this.setState(store.getState()); }); } add(count){ //發起addCount事件 this.props.store.dispatch(addCount(count)); } render(){ return ( <div> <Count store={this.store} count={this.state.count}/> <button onClick={this.add.bind(this,3)}>add</button> </div> ) } } let store= createStore(countReducer,initialState); render( <App store={store}/> , document.getElementById('container') ); 複製程式碼
到這裡,我們的變形金剛已經可以動起來了,可以發起事件,也可以訂閱事件並更新介面。不出意外的話,每次點選add頁面上的數字都能加3了呢。
問題
但是現在還有些問題:
1.如果count的子元件需要使用store,我們得把store作為子元件的props層層傳遞下去
2.現在App可以通過store拿到state中所有的值,也就是state中有任何更新都會導致App重新渲染,但在實際專案中,一個react元件往往只需要state中的某些值。當然你可以在subscribe中拿到新的state後判斷是否需要的屬性發生了改變,然後再去更新介面來規避這個問題,現在,有個外掛react-redux把這些都做好了。
redux-react提供了兩個方法Provider,connect。Provider有一個必要的引數store,它使得所有通過connect生成的子元件能從props中獲得store提供的方法。
首先改寫App和render,將App作為Provider的子元件,並使用connect對原來的App進行改裝
import {render} from 'react-dom'; import{Provider}from 'react-redux'; import{connect}from 'react-redux'; class App extends Component{ addCount(count){ //dispatch是被connect注入到props中的 this.props.dispatch(addCount(count)) } changeName(name){ this.props.dispatch(changeName(name)); } render(){ console.log('app render'); return ( <div> <Countcount={this.props.count}/> <button onClick={this.addCount.bind(this,3)}>add</button> <button onClick={this.changeName.bind(this,'hello')}>changeName</button> </div> ) } } //把state中的值注入到元件的props中 function mapStateToProps(state){ return { //僅把count放到App的props中,在App中就可以使用this.props.count來訪問count了 //並且只有當state中的count發生改變時才會引起app的重新渲染 count:state.count; } } //用connect生成的新元件覆蓋原App,實際上我們在Provider裡使用的是這個App,這一點非常重要 App=connect(mapStateToProps)(App); let store= createStore(countReducer,initialState); render( <Provider store={store}> <App/> </Provider>, document.getElementById('container') ) 複製程式碼
其中mapStateToProps方法以之前定義的state作為入參,返回值可以是整個state,也可以是你需要的部分資料,這裡僅僅把count傳遞給了props,只有count值改變才會引起App的重新渲染,另外這個例子加了一個changeName的按鈕,並在render方法裡打了log來觀察App的重新渲染,點選changeName導致了name值改變,但可以在除錯窗口裡看見並沒有列印“app render”,說明name的改變並沒有引起App的重新渲染。
通過connect我們已經能夠在App中使用this.props.dispatch來發起事件了,但是我們並沒有和一開始一樣在App中寫subscribe訂閱事件,卻依然能監聽到addCount,這是因為connect把訂閱事件也封裝好了,它的原始碼是這樣的:
Connect.prototype.componentDidMount = function componentDidMount() { this.trySubscribe(); }; 複製程式碼
trySubscribe中做了比較計算,只有被mapStateToProps對映到props上的值改變時,才會做setState操作來發起重新渲染。
另外,connect還提供了mapDispatchToProps方法把dispatch事件傳遞給props:
function mapDispatchToProps(dispatch){ return { addCount:function(count){ dispatch(addCount(count)); } } } App=connect(mapStateToProps,mapDispatchToProps)(App); 複製程式碼
這和在App中直接寫add方法是一樣的,這樣我們就可以在App中使用onClick={this.props.addCount}來發起事件了,另外redux還提供一個方法bindActionCreators,把dispatch也給封裝好了,所以上面的mapDispatchToProps還可以寫成這樣:
import {bindActionCreators} from 'redux'; function mapDispatchToProps(dispatch){ return { addCount:bindActionCreators(addCount,dispatch) } } 複製程式碼
這樣看來bindActionCreators這個方法用處似乎不大,但是當你有一組事件都要放入元件的props時,用它就方便很多,你只要把所有定義好的action放在一個物件中傳遞給他就好了:
function mapDispachToProps(dispatch){ //actions是一個物件,裡面包含了一組action return bindActionCreators(actions,dispatch); } 複製程式碼
bindActionCreators的原始碼非常簡單,當actions是一個物件,裡面包含一組動作,bindActionCreators就返回一個物件,類似這樣:
{ addCount:function(){ return dispatch(actions.addCount.apply(undefined, arguments)) }, minusCount:function(){ return dispatch(actions.minusCount.apply(undefined, arguments)) } } 複製程式碼
當actions只是一個單獨的動作,比如addCount,bindActionCreators的返回結果是一個方法:
function(){ return dispatch(addCount.apply(undefined, arguments)) } 複製程式碼
綜上,這些看似高大上的外掛只是把一些麻煩的方法給你封裝好了,如果你不喜歡的話,不用也是完全沒有問題的。