Skip to content
This repository was archived by the owner on Aug 7, 2024. It is now read-only.
This repository was archived by the owner on Aug 7, 2024. It is now read-only.

通过Github Blame深入分析Redux源码 #7

Open
@fi3ework

Description

@fi3ework

说明

本文所分析的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
    }
  }
}

Activity

yzw7489757

yzw7489757 commented on Jul 30, 2019

@yzw7489757

加一句,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 => {
		//...
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @fi3ework@yzw7489757

        Issue actions

          通过Github Blame深入分析Redux源码 · Issue #7 · fi3ework/blog