Redux源码解析(一)

682 阅读5分钟

createStore与combineReducers

Redux思想

Redux是一个Javascript状态容器,提供可预测化的状态管理。对于一个复杂点的应用来说,管理其不断变化的State是很困难的,如果不对状态的修改加以限制,状态之间互相关联影响;那样我们就无法预料一个操作的触发,会带来什么样的变化。

单一数据源

整个应用的state存在于全局唯一的一个Store上,这个唯一的Store是一个树形的对象,里面包含了应用中的所有状态,每个组件往往都是用树形对象上一部分的数据。

保持状态只读

这里说的保持状态只读,意思是不能直接去修改Store中的状态,要想修改应用的state必须通过dispatch派发一个action对象来完成。

数据的修改使用纯函数完成。

这里的纯函数就是reducer(state, action),第一个参数state是当前的状态,第二个参数action是用于dispatch的对象,函数最后返回的新state值完全是根据传入的参数值来确定的。

createStore

Redux库对外暴露的属性不多, createStore就是其中最重要的一个,用于生成我们应用的storestore为用户主要提供以下属性

//删除了enhancer部分源码的createStore
function createStore(reducer, preloadedState) {
  var currentReducer = reducer;//reducer函数
  var currentState = preloadedState;//默认state
  var currentListeners = []; //当前的监听函数
  var nextListeners = currentListeners;
  var isDispatching = false; //应用当前是否正在执行派发操作

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      //数组浅拷贝, 生成一个新的数组, 和以前的数组已经没有关系了, 至于这样做的目的后面会说到
      nextListeners = currentListeners.slice(); 
    }
  }
  function getState() {}
  function dispatch(action) {}
  function subscribe(listener) {}
  
  //用于生成应用默认的state
  dispatch({ type: ActionTypes.INIT }); 
}
  1. getState() 通过闭包技术,获取应用状态

     function getState() {
        if (isDispatching) {...}// 在reducer执行的过程中不可以获取state
        return currentState; //通过闭包技术,在函数外部获取函数函数内定义的局部变量currentState
     }
    
  2. dispatch() 接收一个必须含有type属性的action对象作为参数,用于派发事件,并调用reducer函数,返回一个新的state

    function dispatch(action) {
      	//action必须是一个对象
        if (!isPlainObject(action)) { ... } 
         //action对象必须包含type属性
        if (typeof action.type === 'undefined') { ... }
        //当前不能有正在执行的dispatch操作
        if (isDispatching) { ... } 
        
        try {
          isDispatching = true; 
          //调用reducer返回新的state,其中新state值完全取决于传入的参数currentState、action,所以需要reducer是一个纯函数,才能够使状态的变化可预测、有因可循。
          currentState = currentReducer(currentState, action); 
        } finally {
          isDispatching = false; //reducer函数执行结束,改变状态
        }
    		//createStore源码中有两处currentListeners和nextListeners互相赋值, 另外一处在createStore函数最开始执行时;明明一个变量就能解决问题, 至于为啥源码中要用两个变量来维护listeners,后面会专门解释
        var listeners = currentListeners = nextListeners;
         //遍历监听函数,并依次执行,通知监听的页面状态改变
        for (var i = 0; i < listeners.length; i++) {
          var listener = listeners[i];
          listener();
        }
        return action;
     }
    
  3. subscribe() 订阅函数, 接收一个回调函数作为参数,用于通知组件状态更新了

    function subscribe(listener) {
        if (typeof listener !== 'function') { throw new Error() }// 传入的listener必须是函数
        if (isDispatching) { throw new Error() } //当前不能有正在执行的dispatch操作
        var isSubscribed = true;
      	//保存一份当前的listeners快照,此时currentListeners与currentListeners已经彼此独立,二者指向不同的引用。
        ensureCanMutateNextListeners();
        nextListeners.push(listener); //注意此时nextListeners比currentlisteners新,后者还是未push之前的
        return function unsubscribe() { //返回一个函数用于取消当前的订阅
          if (!isSubscribed) { return;}
          if (isDispatching) { throw new Error() }
          isSubscribed = false;
          ensureCanMutateNextListeners();//保存一份当前的listeners快照
          var index = nextListeners.indexOf(listener);
          nextListeners.splice(index, 1); //在监听数组nextListeners中删除当前监听的函数,
          currentListeners = null;
        };
      }
    
  4. unsubscribe() 取消订阅事件

    订阅函数的返回值是一个函数, 用于取消当前的订阅,直接调用该函数就可以取消订阅

回调上面遗留的问题,监听函数数组listeners,明明用一个变量就可以维护,为啥源码要用两组变量currentListenersnextListeners来维护??在订阅事件和取消订阅事件中都调用了ensureCanMutateNextListeners函数有啥用??

//如果订阅事件内部还有一个订阅事件
store.subscribe(function() {
  console.log(1)
  //在当前这个回调函数执行时,才会执行下面的代码
  store.subscribe(function() { 
    console.log(2)
  })
})


//如果我们模拟执行dispatch, 在订阅事件中不调用`ensureCanMutateNextListeners`函数时, 那么打印的结果会是什么呢?
for (var i = 0; i < listeners.length; i++) {
  var listener = listeners[i];
  listener();
}
// 此时1, 2都会打印
//调用`ensureCanMutateNextListeners`函数时, 只会打印 1

redux的主要目的就是将在dispatch执行过程中产生的订阅和退订事件,不会在当前立即体现, 但是会在下一个dispatch时才体现,所以redux为此做了两件事:

1.每次在执行订阅或退订事件之前生成一份当前的listeners副本,将currentListenersnextListeners分别指向不同的引用,后续监听函数的增删只对nextListeners有效,currentListeners保持不变

2.每次在执行dispatch时,会在reducer函数执行完之后将currentListenersnextListeners指向同一个引用;

结论的得出看似一切都这么顺风顺水,但是我在最开始实验时,遍历执行监听函数用的不是for循环,而是直接用数组的map方法,结果却出人意料,也只打印了1, 即当前的变动没有立即体现。

listeners.map( listener => listener() ) // 只打印了 1

实验一下,如果数组在使用map遍历的过程中,原数组发生了变化,遍历的结果是否会受到影响呢?

var arr1 = [1,2,3,4,5]
var arr2 = []

var arr3 = arr1.map(item => {
  if(item ===1) {
    arr1.push(100)
  }
  return item
})
arr3 // [1, 2, 3, 4, 5] 当前没变化
var arr4 = arr1.map(item => {
  if(item ===1) {
    arr1.push(100)
  }
  return item
})
arr4 // [1, 2, 3, 4, 5, 100]  第二次遍历才有变化

不仅是mapforEach也是这样, 但是我在网上也没有找到原因, 我猜想是不是这些这些方法在封装时,也做了类似redux一样的处理,(当前的变化不立即体现,下一次触发才会体现);

combineReducers

整个应用的state是一个object tree, 里面包含了应用中的所有状态,每个组件中往往都是取之这个大object tree上一部分的数据。而且随着应用越变越复杂, 就需要对庞大的reducer函数进行拆分,让每个子reducer只负责处理object tree中的一部分数据。这样一来,我们就需要一个主函数将这些子reducer生成的子state,组合成一个完整的object tree且结构需要与之前的保持一致。

所以combineReducers要做的事情就是要返回一个函数,返回的这个函数就是我们传传递给createStore的第一个参数 reducer函数,而且返回的这个函数需要将子reducer生成的子state整合并返回。说的有点绕呀~,直接进源码吧

function combineReducers(reducers) {//注意这里的reducers是由一系列子reducer作为键值构成的对象
  var reducerKeys = Object.keys(reducers); //获取对象的键名
  var finalReducers = {}; //浅拷贝最终所有满足条件的reducer

  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];
    if (typeof reducers[key] === 'function') { //reducer必须是函数,
      finalReducers[key] = reducers[key];  //函数浅拷贝
    }
  }
  var finalReducerKeys = Object.keys(finalReducers); //获取最终的reducer键名

  return function combination(state, action) { //接收一个state和action?? 这不就是reducer函数么
    //void 0 等同于 undefined, 用void 0 替代 undefined的好处有两个, 在局部作用域中undefined可能会被当成变量重写; 相比undefined9个字符void 0更少呀;
    if (state === void 0) { 
      state = {};
    }

    var hasChanged = false; //用于判断状态是否改变
    var nextState = {};

    //
    for (var _i = 0; _i < finalReducerKeys.length; _i++) { 
      var _key = finalReducerKeys[_i]; //获取kek
      var reducer = finalReducers[_key]; //根据key获取对应的子reducer
      var previousStateForKey = state[_key]; //根据key获取此时对应的子state
      var nextStateForKey = reducer(previousStateForKey, action);//调用reducer函数,返回新的state
      nextState[_key] = nextStateForKey; //根据reducerKey组合State
      //浅比较判断当前子state是否变化, 遍历过程中只要有一次hasChanged为true,那么最终hasChanged就为true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
		//假如默认的state是一个空对象{},而且经子reducer计算返回的值也是undefined时,上面的hasChanged就判断不了啦, 因为nextStateForKey 和 previousStateForKey都是undefined。所以下面这行代码是针对那些原state中没有定义的属性, 但经reducer计算处理后依然返回undefined的情况
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
}