开始

今天分析的源码就是大名鼎鼎的Redux,有React的地方就有它的身影,一开始感觉这么有名的库,代码应该挺多挺复杂的,但是出乎我意料的是,它的实现非常简洁,核心代码可能也就在两百行左右,我似乎明白它一直流行的原因,不是实现有多复杂精妙而是一种简洁,keep it simple!

分析

首先先熟悉一个很重要的概念:结构共享,其实也很简单,例如假设有一个这样的状态树:
AAD2D192-5ADA-4EFB-B6C0-B964A7CA0E28.png
当我们把状态C换成状态E, 那么就重新生成一棵状态树:
57FC3838-D6D6-4912-9D15-5FC07DBB9B47.png
可以看到新的树里面所有节点都并不都是新创建的,只有E,C',Root'是新建的节点,而B和D都是引用原本的节点,这就是所谓的结构共享,当某个状态节点修改的时候,一路往上直到根节点,都需要重新创建;所以当我们拿到新的状态树引用的时候,只要通过遍历和简单的比较引用就可以发现状态树那些部分是修改过,而最终可以定位到被修改过的节点的然后进行相应的更新。

明白了主要的概念之后,我们接着代码分析。
直接从一段代码开始入手吧:

import { createStore, combineReducer applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';

const store = createStore(
  combineReducers({
    cart: (state = [], action)=> {
        if(action.type === 'ADD_PRODUCT') {
            return [
                ...state,
                action.product
            ]
        }
        return state;
    }
    user: combineReducers({
        info: (state = {}, action)=> {
            if(action.type === 'UPDATE_INFO') {
                return action.info;
            }
            return state;
        }
    })
  }),
  applyMiddleware(thunk, createLogger())
);

我们创建了一个store,而里面的状态有两个,一个是cart(购物车),而它响应的action也只有一个就是添加商品;而另外一个是user,它的目的是为了分析嵌套的combineReducers。

好吧,准备工作完成,按照代码调用顺序先分析combineReducers函数:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
  
  ...

  return function combination(state = {}, action) {
    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

首先combineReducers返回的也是reducer,正如名称一样,它是可以组合其他reducer的reducer;
可以看到combineReducers会先遍历reducers主要为了找出对象里面的reducer(只要是function都判断为是reducer),然后返回一个可以接受state和action的函数combination;
再来分析combination,当接收到根state和触发的action后,先初始化一个默认的nextState对象(所以combineReducers只能传入一个对象,不能是数组)用来接收子reducer产生的state,然后就开始遍历finalReducerKeys依次调用子reducer,并传人对应的子state和action,如果子reducer根据action判断后返回了一个新的子state,那么就把hasChanged设置为true表明有子state改变了,最后返回的时候会根据这个标记位来判断是否要返回新的nextState,还是旧的state。
这样这个函数其实就已经完成从旧的state树生成一棵新的state树并且能够实现刚才所说的结构共享。
代码十分简短,可以说是一目了然,当然它的state树的维护是要靠每个reducer遵循一个协议,有新的改变的时候必须要返回一个新的对象,就像一开始的初始化代码那样,如果添加商品的时候是在原来的数组上添加的并没有新创建数组,就能触发新的state树生成了,因为combineReducers这里无法知道子state发生变化了。

接着看createStore函数:

function createStore(reducer, preloadedState, enhancer) {
    let currentReducer = reducer
    let currentState = preloadedState
    function dispatch(action) {
        ...

        try {
          isDispatching = true
          currentState = currentReducer(currentState, action)
        } finally {
          isDispatching = false
        }

        ...

        return action
  }
  
  dispatch({ type: ActionTypes.INIT })
  ...
}

感觉createStore这个方法其实也很简单了,因为刚刚combineReducers其实已经完成了最核心的工作了,createStore初始化的时候只需要dispach一个INIT的action然后就是调用根reducer,传入初始化的state和action产生新的根state,初始化的工作就完成了。

最后再分析一下Redux的中间件机制的实现吧,对于中间件机制的实现,axios利用的是Promise链,而Redux则是利用简单的函数回调。
直接看applyMiddleware函数的实现:

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

首先这个函数对于dispatch变量的处理,一开始dispatch绑定的是一个直接抛出错误的方法,目的是为了阻止开发者在构建中间件的时候调用dispatch,当中间件链构建完之后dispatch才绑定最后生成的中间件链;所以后面在中间件里面调用的dispatch其实都会重新走一遍中间件链。

那么中间件链是如何构建出来的尼,关键点就是compose函数:

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

正如源码注释所说举的例子,compose的作用就是可以把compose(f, g, h)转变成(...args) => f(g(h(...args)))一个这样的函数;但是这样看好像跟中间件链完全靠不上边,只是仅仅简单把h函数返回的值传给g函数,g函数又把自己调用的结果传给f函数,完全不是f->g->h这样的链式调用。但是如果f,g,h是以下这样的定义形式那就完全不一样了:

(next)=> (params)=> {
    next();
}

h会把一个函数作为g的参数next传给g,g又会生成一个函数作为f的参数next传给f,就能够实现f->g->h这样的链式调用了。

再看看redux-thunk这个中间件的怎么定义的:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

({ dispatch, getState }) => next => action => {}
dispatch和getState参数会在applyMiddleware函数中获取:

 const chain = middlewares.map(middleware => middleware(middlewareAPI))

然后就是返回一个刚刚所说的类似函数定义形式。
而redux-thunk的作用就是判断如果action是一个函数的话,就把dispatch和getState传给它,并且中止后面的中间件链式调用,这就阻止最后真正的dispatch调用了,而action函数里面也接收到dispatch函数可以进行异步或同步dispatch。

最后

Redux的分析就结束了,总体来说这个库真的简短有力,在分析的过程中确确实实的感受到一种优雅,如同诗一般,什么时候我的代码也能做到这样尼,路漫漫其修远兮。。。


tain335
576 声望196 粉丝

Keep it simple.