Redux:全揭秘与入坑指北(上)

1,166 阅读5分钟
  • createStore.js
    • 关于state初始值
  • combinReducer.js
    • 一个reducer对应state里的一个属性对应一个组件
  • dispatch [dɪ'spætʃ]
    • bindActionCreators.js
  • react-redux
    • Provider.js
    • connect.js

pre-notify

emmm...这是一篇偏重于源码实现的文章,其次是使用的注意事项,阅读请谨慎。

先上一张广为流传的总览图:

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的

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分发给对应的组件。

注意: mapStateToProps在上栗中是state=>state.某组件对应的state属性名的形式,但也有可能我们只有一个组件,没有使用combinReducers,这意味着我们的state中的数据结构只会有一层,即这个一个组件下的所有属性,So,这样的情况下我们的mapStateToProps函数应该是这样的state=>state or state=>{...state}


参考:

=== ToBeContinue ===