Redux:全揭祕與入坑指北(上)

Cris_冷崢子發表於2019-03-03
  • createStore.js
    • 關於state初始值
  • combinReducer.js
    • 一個reducer對應state裡的一個屬性對應一個元件
  • dispatch [dɪ'spætʃ]
    • bindActionCreators.js
  • react-redux
    • Provider.js
    • connect.js

pre-notify

emmm...這是一篇偏重於原始碼實現的文章,其次是使用的注意事項,閱讀請謹慎。

先上一張廣為流傳的總覽圖:

Redux:全揭祕與入坑指北(上)

createStore.js

//createStore.js

// getState:獲取倉庫裡儲存的state
// dispatch:執行動作,更新state(即使state沒有變化)
// subscribe:註冊更新完state後要執行的cb

export default function createStore(reducer,preloadedState){
	let state = preloadedState;
    let listeners = [];
    
    dispatch({}); //createStore時手動呼叫一次,給state賦上初始值
    
    function dispatch(action){
    	state = reducer(state,action);
        listeners.forEach(cb=>cb());
    }
    
    function getState(){
    	return JSON.parse(JSON.stringify(state));
    }
    
    function subscribe(listener){
    	listeners.push(listener);
        return function(){
        	listeners = listeners.filter(item=>item!=listener);
        }
    }
    
    return {getState,dispatch,subscribe}
}
複製程式碼

關於state初始值

關於state初始值的,在上面相應的程式碼示例部分已經做出了相應的註釋。

另外需要注意的是當我們呼叫createStore()初始化一個倉庫時,可以傳入一個preloadedState引數作為createStore的第二個引數傳入,它也能讓倉庫的state初始化。

export default function createStore(reducer,preloadedState){
    let state = preloadedState;
    ...
複製程式碼

假若像上面這樣初始化了,那麼我們在每個reducer中寫的各自的initState就不再有效果。

// 某個元件的reducer

let initState = {
  number:0
};

export default function reducer(state = initState,action){
  switch(action.type){
  ...
複製程式碼

So,我們即可以在createStore時候傳入一個物件(preloadedState)統一初始化所有元件的state,也可以選擇在各自元件對應的reducer中初始化各自的initState

combinReducer.js

// combinReducers.js

function combineReducers(reducers){
	return function(state = {},action){ //state={}這裡賦了{},只是為了相容state[attr]不報錯
    	let newState = {};
        for(let attr in reducers){
    	    let reducer = reducers[attr];
            newState[attr] = reducer(state[attr],action); //state[attr]可能為undefined,我們一般會在每個reducer裡賦一個initState的初始值
        }
        return newState;
    }
}

// --- --- ---

//以下為高逼格版
export default reducers=>(state={},action)=>Object.keys(reducers).reduce((currentState,key)=>{
  currentState[key] = reducers[key](state[key],action);
  return currentState;
},{});
複製程式碼

一個reducer對應state裡的一個屬性對應一個元件

一般將各個reducer放在一個名為reducers的資料夾下,並且又該資料夾下的index.js檔案統一匯出。

//reducers/index.js

import counter from './counter';
import counter2 from './counter2';
import {combineReducers} from '../../redux'
// 合併後應該返回一個新的函式
export default combineReducers({
  counter
  ,counter2
});
複製程式碼

當呼叫dispatch時是這麼運轉從而改變原state的

Redux:全揭祕與入坑指北(上)

So,如果同一個倉庫裡的一個元件觸發了動作(比如說A),而另一個元件(比如說B)沒有觸發,雖然都會執行一次各自的reducer,但由於reducer裡的動作是不能重名的(A元件和B元件),So,B元件在自己的reducer中是找不到A裡的動作的,逛了一圈就會出去,不會對原狀態有任何影響。

// createStore裡的state長這樣

{
    counter:{number:0} //元件1的狀態資料
    ,counter2:{number:0} //元件2的狀態資料
}
複製程式碼

我們能在元件裡這樣拿到

//store/index.js

import {createStore} from '../redux';
import reducer from './reducers';

let store = createStore(reducer); // {getState,dispatch,subscribe}
export default store;

// --- --- ---

// 一個元件中
import store from '../store';

...
this.state = {number:store.getState().counter2.number};
...
複製程式碼

dispatch [dɪ'spætʃ]

dispatch,派發的意思,它是createSotre產出的一個方法。

嗯,派發,派發什麼呢?dispatch是專門用來派發action/動作的,每個action都會改變state上某個元件對應的屬性上的某屬性。

最原始的派發任務(action)是這樣的

...
<button onClick={()=>store.dispatch({type:types.INCREMENT})}>+</button>
...
複製程式碼

但我們在專案中一般會建立一個actions檔案件,然後在裡面按照不同的元件來建立一個模組,這個模組存放著這個元件所有的action

// store/actions/counter.js

import * as types from '../action-types';

//actionCreator 建立action的函式
export default {
  increment(){
    return {type:types.INCREMENT}
  }
  ,decrement(){
    return {type:types.DECREMENT}
  }
}
複製程式碼

於是乎,派發時就變成這樣

...
import actions from '../store/actions/counter';
...
<button onClick={()=>store.dispatch(actions.increment())}>+</button>
...
複製程式碼

bindActionCreators.js

emmm...有些人覺得上面的派發寫起來仍然很費勁,於是乎就寫了這麼個模組(別問我這些貨是怎麼想的)

//bindActionCreators.js

export default function bindActionCreators(actions,dispatch){
  let newActions = {};
  for(let attr in actions){
    newActions[attr] = function(){
      // actions[attr] => increment(){return {type:types.INCREMENT}}
      dispatch(actions[attr].apply(null,arguments));
    }
  }
  return newActions;
}
複製程式碼

於是乎我們最終派發寫起來是這樣的

...
import actions from '../store/actions/counter';
...
let newActions = bindActionCreators(actions,store.dispatch);
...
<button onClick={newActions.increment}>+</button>
...
複製程式碼

react-redux

我們在react使用redux時,其實有許多程式碼是冗餘的

比如

...
componentDidMount(){
    this.unsubscribe = store.subscribe(()=>{
      this.setState({number:store.getState().counter.number});
    });
}
componentWillUnmount(){
    this.unsubscribe();
}
...
複製程式碼

再比如

constructor(){
    super();
    this.state = {number:store.getState().counter2.number};
  }
複製程式碼

又或則

import {bindActionCreators} from '../redux'
let newActions = bindActionCreators(actions,store.dispatch);
複製程式碼

So,react-redux的作用就是把這些冗餘的程式碼抽離成一個模板出來,嗯,弄一個高階元件。

Provider.js

這個元件主要是以便將store傳遞給子孫元件

import React,{Component}from 'react';
import propTypes from 'prop-types';

export default class Provider extends Component{
  static childContextTypes = {
    store:propTypes.object.isRequired
  };
  getChildContext(){
    return {store:this.props.store};
  }
  render(){
    return this.props.children;
  }
}
複製程式碼

我們一般是這樣使用的

...
import store from './redux2/store'
...
  <Provider store={store}>
    <React.Fragment>
      <Component1/>
      <Component2/>
    </React.Fragment>
  </Provider>
...
複製程式碼

connect.js

首先我們一般在元件中這樣呼叫這個高階元件

//Counter.js 元件中

export default connect(
  state=>state.counter
  ,actions
)(Counter);
複製程式碼

其中第一個引數是為了過濾倉庫中非該元件資料的其它資料。

第二個引數actions是元件自己的動作,有兩種可選的傳遞形式:

  • 物件的形式
//actionCreator【物件】,用來建立action函式
//即之前的actions資料夾下的每個元件的action檔案
{
  increment(){
    return {type:types.INCREMENT}
  }
  ,decrement(){
    return {type:types.DECREMENT}
  }
}
複製程式碼
  • 函式的形式
//即之前經過bindActionCreators 處理過後的 actions
let mapDispatchToProps = dispatch => ({
  increment:()=>dispatch({type:types.INCREMENT})
  ,...
});
複製程式碼

其中物件的形式在高階元件中會被轉換為第二種函式形式執行後的樣子。

//connect.js

export default function(mapStateToProps,mapDispatchToProps){
	return function(WrappedComponent){
    	class ProxyComponent extends Component{
        	static contextTypes = {
            	store:propTypes.object
            }
            constructor(props,context){
            	super(props,context);
                this.store = context.store;
                this.state = mapStateToProps(this.store.getState());
            }
            componentDidMount(){
            	this.unsubscribe = this.store.subscribe(()=>{
                	this.setState(mapStateToProps(this.store.getState()));
                });
            }
            componentWillUnmount(){
            	this.unsubscribe();
            }
            render(){
            	let actions = {};
                if(typeof mapDispatchToProps === 'function'){
                	actions = mapDispatchToProps(this.store.dispatch);
                }else if(typeof mapDispatchToProps === 'object'){
                	actions = bindActionCreators(mapDispatchToProps,this.store.dispatch);
                }
                return <WrappedComponent {...this.state} {...actions}>
            }
        }
        return ProxyComponent;
    }
}
複製程式碼

經過高階元件包裝後,每個元件都只會擁有倉庫中屬於自己的那部分資料,並且屬於每個元件的動作還會作為props分發給對應的元件。

Redux:全揭祕與入坑指北(上)

注意: mapStateToProps在上慄中是state=>state.某元件對應的state屬性名的形式,但也有可能我們只有一個元件,沒有使用combinReducers,這意味著我們的state中的資料結構只會有一層,即這個一個元件下的所有屬性,So,這樣的情況下我們的mapStateToProps函式應該是這樣的state=>state or state=>{...state}


參考:

=== ToBeContinue ===

相關文章