fi3ework / blog

📝

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

通过Github Blame深入分析Redux源码

fi3ework opened this issue · comments

commented

说明

本文所分析的Redux版本为3.7.2

分析直接写在了注释里,放在了GitHub上 —> 仓库地址

分析代码时通过查看Github blame,参考了Redux的issue及PR来分析各个函数的意图而不仅是从代码层面分析函数的作用,并且分析了很多细节层面上写法的原因,比如:

  1. dispatch: (...args) => dispatch(…args) 为什么不只传递一个 action ?

  2. listener 的调用为什么从 forEach 改成了 for ?

  3. 为什么在 reducer 的调用过程中不允许 dispatch(action) ?

    ...

水平有限,有写的不好或不对的地方请指出,欢迎留issue交流😆

文件结构

Redux的文件结构并不复杂,每个文件就是一个对外导出的函数,依赖很少,分析起来也比较容易,只要会用Redux基本上都能看懂本文。
这是Redux的目录结构:

.
├── applyMiddleware.js       将middleware串联起来生成一个更强大的dispatch函数,就是中间件的本质作用
├── bindActionCreators.js    把action creators转成拥有同名keys的对象
├── combineReducers.js       将多个reducer组合起来,每一个reducer独立管理自己对应的state
├── compose.js               将middleware从右向左依次调用,函数式编程中的常用方法,被applyMiddleware调用
├── createStore.js           最核心功能,创建一个store,包括实现了subscribe, unsubscribe, dispatch及state的储存
├── index.js                 对外export
└── utils                    一些小的辅助函数供其他的函数调用
   ├── actionTypes.js        redux内置的action,用来初始化initialState
   ├── isPlainObject.js      用来判断是否为单纯对象
   └── warning.js            报错提示

源码分析

源码分析的顺序推荐如下,就是跟着pipeline的顺序来

index.js -> createStore.js -> applyMiddleware.js (compose.js) -> combineReducers.js -> bindActionCreators.js

主题思路我会写出来,很细节的部分就直接写在代码注释里了。

index

function isCrushed () {}

// 如果使用minified的redux代码会降低性能。
// 这里的isCrushed函数主要是为了验证在非生产环境下的redux代码是否被minified
// 如果被压缩了那么isCrushed.name !== 'isCrushed'
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' && // 有的浏览器(IE)并不支持Function.name,必须判断先判断是否支持Function.name,才能判断是否minified
  isCrushed.name !== 'isCrushed'
) {
  warning(
    "...'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

只有两个功能:

  1. 区分开发环境和生产环境
  2. 对外暴露API,相当简洁,常用的API只有五个

createStore

createStore 由于有两种生成 store 的方法,所以起手先确定各个参数

  // 传递两个参数时,实际传递的是 reducer 和 enhancer,preloadedState 为 undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 传递三个参数时,传递的是 reducer preloadedState enhancer(enhancer必须为函数)
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 如果传入了 enhancer(一个组合 store creator 的高阶函数)则控制反转,交由enhancer来加强要生成的store
    // 再对这个加强后的 store 传递 reducer 和 preloadedState
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 传入的reducer必须是一个纯函数,且是必填参数
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

然后声明中间变量,后面会讲到这些中间变量的作用

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

然后看怎么订阅一个事件,其实这就是一个发布-订阅模式,但是和普通的发布订阅模式不同的是, 多了一个ensureCanMutateNextListeners 函数。 我去翻了一下redux的commit message,找到了对listener做深拷贝的原因:https://github.com/reactjs/redux/issues/461,简单来说就是在listener中可能有unsubscribe操作,比如有3个listener(下标0,1,2),在第2个listener执行时unsubscribe了自己,那么第3个listener的下标就变成了1,但是for循环下一轮的下标是2,第3个listener就被跳过了,所以执行一次深拷贝,即使在listener过程中unsubscribe了也是更改的nextListeners(nextListeners会去深拷贝currentListeners)。当前执行的currentListeners不会被修改,也就是所谓的快照。

redux在执行subscribe和unsubscribe的时候都要执行ensureCanMutateNextListeners来确定是否要进行一次深拷贝,只要执行dispatch,那么就会被const listeners = (currentListeners = nextListeners),所以currentListeners === nextListeners,之后的subscribe和unsubscribe就必须深拷贝一次, 否则可以一直对nextListeners操作而不需要为currentListeners拷贝赋值,即只在必要时拷贝。

  function subscribe (listener) {
    // 传入的listener必须是一个可以调用的函数,否则报错
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }
    // 同上,保证纯函数不带来副作用
    if (isDispatching) {
      throw new Error(
        '...'
      )
    }

    let isSubscribed = true

    // 在每次subscribe的时候,nextListenerx先拷贝currentListeners,再push新的listener
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe () {
      if (!isSubscribed) {
        return
      }

      // 同上,保证纯函数不带来副作用
      if (isDispatching) {
        throw new Error(
          '...'
        )
      }

      isSubscribed = false

      // 在每次unsubscribe的时候,深拷贝一次currentListeners,再对nextListeners取消订阅当前listener
      ensureCanMutateNextListeners()
      // 从nextListeners中去掉unsubscribe的listener
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

接下来看 dispatch 这个函数,可以看到每次dispatch时会const listeners = (currentListeners = nextListeners),为可能到来的mutateNextListener做好准备。

  function dispatch (action) {
    // action必须是一个plain object,如果想要能处理传进来的函数的话必须使用中间件(redux-thunk等)
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // action必须定义type属性
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 同上,保证纯函数不带来副作用
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    // currentReducer不可预料是否会报错,所以try,但不catch
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      // 必须在结束的时候将isDispatching归位
      isDispatching = false
    }

    // 在这里体现了currentListeners和nextListeners的作用
    const listeners = (currentListeners = nextListeners)
    // 这里使用for而不是forEach,是因为listeners是我们自己创造的,不存在稀疏组的情况,所有直接用for性能来得更好
    // 见 https://github.com/reactjs/redux/commit/5b586080b43ca233f78d56cbadf706c933fefd19
    // 附上Dan的原话:This is an optimization because forEach() has more complicated logic per spec to deal with sparse arrays. Also it's better to not allocate a function when we can easily avoid that.
    // 这里没有缓存listeners.length,Dan相信V8足够智能会自动缓存,相比手工缓存性能更好
    for (let i = 0; i < listeners.length; i++) {
      // 这里将listener单独新建一个变量而不是listener[i]()
      // 是因为直接listeners[i]()会把listeners作为this泄漏,而赋值为listener()后this指向全局变量
      // https://github.com/reactjs/redux/commit/8e82c15f1288a0a5c5c886ffd87e7e73dc0103e1
      const listener = listeners[i]
      listener()
    }

    return action
  }

接下来看getState,就是一个return

  function getState () {
    // 参考:https://github.com/reactjs/redux/issues/1568
    // 为了保持reducer的pure,禁止在reducer中调用getState
    // 纯函数reducer要求根据一定的输入即能得到确定的输出,所以禁止了getState,subscribe,unsubscribe和dispatch等会带来副作用的行为
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

observable函数,这个是为了配合RxJS使用,如果不使用RxJS可以忽略,在这里略过。

replaceReducer函数是替换整个store的reducer,一般不经常用到,代码也含简单,换个reducer重新init一下

  function replaceReducer (nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    // ActionTypes.REPLACE其实就是ActionTypes.INIT
    // 重新INIT依次是为了获取新的reducer中的默认参数
    dispatch({ type: ActionTypes.REPLACE })
  }

最后,暴露的接口的功能都已经具备了,还需要取一下默认值,你可能会说不是已经有preloadedState了吗但是默认值不是只有一个的,每个reducer都可以指定对应部分的state的默认值,那些默认值需要先经过一个action的洗礼才可以被赋值,还记得reducer要求每个不可识别的action.type返回原始state吗?就是为了取得默认值。

  // reducer要求对无法识别的action返回state,就是因为需要通过ActionTypes.INIT获取默认参数值并返回
  // 当initailState和reducer的参数默认值都存在的时候,参数默认值将不起作用
  // 因为在调用初始化的action前currState就已经被赋值了initialState
  // 同时这个initialState也是服务端渲染的初始状态入口
  dispatch({ type: ActionTypes.INIT })

为了保证这个type是无法识别的,被定义成了一个随机值

const ActionTypes = {
  // INIT和REPLACE一模一样,只是含义不同,REPLACE其实就是INIT
  INIT:
    '@@redux/INIT' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.'),
  REPLACE:
    '@@redux/REPLACE' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.')
}

至此,我们的已经能够createStore,getState,subscribe,unsubscribe,dispatch了

combineReducer

combineReducer的代码挺长的,但是主要都是用来检查错误了,核心代码就是将要合并的代码组织组织成一个树结构,然后将传入的reduce挨个跑action,跑出的新的state替换掉原来的state,因为无法识别的action会返回原来的state,所以大部分无关的reducer会返回相同引用的state,只有真正捕获action的reducer会返回新的state,这样做到了局部更新,否则每次state的一部分更新导致所有的state都原地深拷贝一次就麻烦了。

export default function combineReducers (reducers) {
  // 第一次筛选:将reducers中为function的属性赋值给finalReducers
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

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

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  // 用来检查reducer是否会返回undefined
  // 因为combineReducers有可能会嵌套多层,当嵌套的某一层如果返回undefined
  // 那么当访问这一层的子reducer的时候就会发生TypeError的错误
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // combination:组合起来的reducer
  return function combination (state = {}, action) {
    // 如果之前的reducer检查不合法,则throw错误
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    // 检查excepted state并打印错误
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    //
    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)
      // 不允许任何action返回undefined
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }

bindActionCreators

这个用的不多,一般是为了方便,直接import *来引入多个actionCreators,原理很简单:实际上就是返回一个高阶函数,通过闭包引用,将 dispatch 给隐藏了起来,正常操作是发起一个 dispatch(action),但bindActionCreators 将 dispatch 隐藏,当执行bindActionCreators返回的函数时,就会dispatch(actionCreators(...arguments))。所以参数叫做 actionCreators,作用是返回一个 action
如果是一个对象里有多个 actionCreators 的话,就会类似 map 函数返回一个对应的对象,每个 key 对应的 value 就是上面所说的被绑定了的函数。

// 真正需要获取参数的函数被柯里化了起来
function bindActionCreator (actionCreator, dispatch) {
  // 高阶函数,闭包引用 dispatch
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}



export default function bindActionCreators (actionCreators, dispatch) {
  // 如果是actionCreators是函数,那么直接调用,比如是个需要被thunk的函数�
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `...`
    )
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      // 每个 key 再次调用一次 bindActionCreator
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  // map 后的对象
  return boundActionCreators
}

applyMiddleware

精髓来了,这个函数最短但是最精髓,这个 middleware 的洋葱模型的**是从koa的中间件拿过来的,我没看过koa的中间件(因为我连koa都没用过...),但是重要的是**。

放上redux的洋葱模型的示意图(via 中间件的洋葱模型

            --------------------------------------
            |            middleware1              |
            |    ----------------------------     |
            |    |       middleware2         |    |
            |    |    -------------------    |    |
            |    |    |  middleware3    |    |    |
            |    |    |                 |    |    |
          next next next  ———————————   |    |    |
dispatch  —————————————> |  reducer  | — 收尾工作->|
nextState <————————————— |     G     |  |    |    |
            | A  | C  | E ——————————— F |  D |  B |
            |    |    |                 |    |    |
            |    |    -------------------    |    |
            |    ----------------------------     |
            --------------------------------------


顺序 A -> C -> E -> G -> F -> D -> B
    \---------------/   \----------/
            ↓                ↓
      更新 state 完毕      收尾工作

单独理解太晦涩,放一个最简单的redux-thunk帮助理解。

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;

最难理解的就是那三个柯里化箭头,这三个箭头相当于欠了真正的middleware实体三个参数{dispatch, getState}nextaction,作为一个中间件,就是将这几个像管道一样在各个中间件中传递,与此同时加上一些副作用,比如后续管道的走向或者发起异步请求等等。

那么开始看欠的这三个参数是怎么还给中间件的,代码不长,所以直接写在注释里了,一行一行看就可以。

applyMiddleware:

export default function applyMiddleware (...middlewares) {
  // 传入createStore
  return createStore => (...args) => {
    // 先用传入的createStore来创建一个最普通的store
    const store = createStore(...args)
    // 初始化dispatch,记住这个dispatch是最终我们要将各个中间件串联起来的dispatch
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    // 储存将要被串联起来的中间件,函数签名为next => action => {...}
    // 下一个中间件作为next传进去,被当前中间件调用
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
        
      // 在这里dispatch使用匿名函数是为了能在middleware中调用最新的dispatch(闭包):
      // 必须是匿名函数而不是直接写成dispatch: store.dispatch
      // 这样能保证在middleware中传入的dispatch都通过闭包引用着最终compose出来的dispatch
      // 如果直接写成store.dispatch,在`dispatch = compose(...chain)(store.dispatch)`中
      // middlewareAPI.dispatch并没有得到更新,依旧是最老的,只有在最后才得到了更新
      // 但是我们要保证在整个中间件的调用过程中,任何中间件调用的都是最终的dispatch
      // 我写了个模拟的调用,可以在 http://jsbin.com/fezitiwike/edit?js,console 上感受一下

      // 还有,这里使用了...args而不是action,是因为有个PR https://github.com/reactjs/redux/pull/2560
      // 这个PR的作者认为在dispatch时需要提供多个参数,像这样`dispatch(action, option)`
      // 这种情况确实存在,但是只有当这个需提供多参数的中间件是第一个被调用的中间件时(即在middlewares数组中排最后)才有效
      // 因为无法保证上一个调用这个多参数中间件的中间件是使用的next(action)或是next(...args)来调用
      // 在这个PR的讨论中可以看到Dan对这个改动持保留意见
      dispatch: (...args) => dispatch(...args)
    }
    // 还了 {dispatch, getState}
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 还了 next
    // 最开始的 next 就是 store.dispatch
    // 相当于就是每个中间件在自己的过程中做一些操作,做完之后调用下一个中间件(next(action))
    dispatch = compose(...chain)(store.dispatch)

    // 最终返回一个dispatch被修改了的store,这个dispatch串联起了中间件
    // 欠的那个action会在dispatch的时候传入
    return {
      ...store,
      dispatch
    }
  }
}

加一句,redux的执行流程与koa的一样,但是在middleware里再调用dispatch就是重置当前任务流 A->B->C(调用dispatch) ->A->B->D

C的那步必须做条件判断,否则死循环,类似的使用场景有

const loggerHistory = store => {
	history.listen((location) => {
		store.dispatch({type: 'history', payload: location})
	})
	return next => action => {
		//...
	}
}