Redux 源码解析

Redux 源码解析

  • 闲谈
  • Redux
    • Redux 源码结构
    • 工具函数
    • CreateStore
    • bindActionCreators
    • combineReducers
    • ★ applyMiddleware
  • 总结

闲谈

好些个日子没有写博客了,脑子里头想着有好些事情该干的该写的,但平日里就又陷入实验室与课程所带来无穷的琐事中了。偶尔偷得闲,还是写两篇罢,老是囤在笔记里头日晒不着的,眼瞅着就要发霉生虫了。

Redux

Redux源码的设计简介明白,拢共加起来也不到千行,注释写的也清清楚楚,只要心里头对于Redux的设计理念有个大致的了解,源码就能很轻松的阅读。

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。

官网的这两句介绍就把Redux的来龙去脉介绍的一清二楚了,要理解Redux的设计思路,首先得对Redux的核心概念和函数式编程思想有一定的认知。
这儿就不对这些预备知识进行介绍了,Redux文档中已经给出了非常细致的介绍(其实Redux文档中涵盖了方方面面的介绍,例如核心思想、使用技巧、API文档等,这些内容比单单分析Redux源码更有价值)。函数式编程在Redux源码中大概体现在Currying、Compose以及纯函数,只需花10分钟简单了解即可。

Redux 源码结构

首先来看一眼Redux源码的文件结构:
Redux 源码解析_第1张图片
文件结构很清爽,包含了6个核心文件和3个工具文件,简单列个表来看看其大致功能:

文件 功能 备注
index.js 入口文件
createStore.js 提供 createStore API
compose.js 函数组合 虽然也是工具文件的功能但是不知道为什么不放在 utils 目录下
combineReducers.js 提供了 combineReducers API,同时包含了一些 reducer 的验证函数
bindActionCreators.js 提供了 bindActionCreators API
applyMiddleware.js 提供了 applyMiddleware API
warning.js 显示 warning 提示信息 工具文件
isPlainObject.js 判断对象是否是一个普通对象 工具文件
actionTypes.js 提供预设的 action 工具文件

工具函数

工程中所使用的工具文件包含在 utils文件夹下(compose.js也算在里头),简单介绍其中的一部分:

// compose.js

// Redux 中 compose 函数和函数式编程中是同个概念,就是对函数进行组合
// compose(f, g) --> f(g)
export default 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)))
}
// actionTypes.js

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`, // store 初始化所触发的 action
  REPLACE: `@@redux/REPLACE${randomString()}`, // store 中 reducer 被热替换所触发的 action
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` // 未知的 action
}

actionTypes 中所定义的三个预设 action 主要在 createStore以及 combineReducers中有所使用,详见后续章节内容。

CreateStore

createStore是Redux所提供的API之一,其用于生成唯一的 store 并提供对应的 dispatchsubscribe等方法。Redux 中的 store 仅能通过 dispatch函数触发对应的 action 来改变,action 对应到纯函数的 Reducer 从而修改 store 的数据。createStore的一个使用示例如下:

let store = createStore(myReducer, {}, applyMiddleware(thunk, logger));

createStore接收三个参数:reducer, preloadedState, enhancer,其中reducer参数表示对store进行修改的 reducer 函数,其可由combineReducers函数组合多个 reducer 生成。preloadedState代表 store 的初始化状态,该参数可以省略,当其被省略时,输入参数为:reducer, enhancerenhancer是中间件通过applyMiddleware方法所生成的对createStore进行功能加强的函数。具体代码解析如下:

export default function createStore(reducer, preloadedState, enhancer) {
  // 处理传入多个 enhancer 的错误
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error('balabala')
  }

  // 当传入两个参数时,其输入参数的含义为 (reducer, enhancer)
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    // enhancer 必须是一个函数
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 使用 enhancer 对 createStore 功能进行加强,enhancer 的具体执行功能见 applyMiddlerware
    return enhancer(createStore)(reducer, preloadedState)
  }

  // reducer 同样必须为函数,为什么不把这一步判断提前到 enhancer 前头?
  // 这样不就留下了到 enhancer 执行完后再处理了吗?
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer // 保存 store 对应的 reducer
  let currentState = preloadedState // 创建时的当前状态即为初始状态
  let currentListeners = []  // 记录对于 store 状态变化的 listeners
  let nextListeners = currentListeners /// 通过两个数组保存 listerners 以保证执行过程的有序,详见后续说明
  let isDispatching = false // 是否正在 dispatch 的标识符

  // 用于保证 nextListeners 和 currentListeners 不会指向同个数组
  function ensureCanMutateNextListeners() {
    // 当 nextListeners 和 currentListeners 指向同个数组时,通过 slice 来创建一个副本
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回当前 state 
  function getState() {
    // 当 dispatch 正在执行时报错
    if (isDispatching) {
      throw new Error('balabala')
    }

    return currentState
  }

  // 对 store 的变化进行订阅
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      // 同样不允许在 dispatch 执行过程中订阅
      throw new Error('balabala')
    }

    let isSubscribed = true

    // 保证 nextListeners 和 currentListeners 不指向同个数组,
    // 同时,所有的新添加的 listener 都放到 next 数组中
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    // 与常见的发布订阅的实现相同,返回一个取消订阅的方法
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error('balabala')
      }

      isSubscribed = false

      // 与添加 listener 相同,仅在 next 数组上执行删除操作
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  // 触发 action 从而修改 store 数据
  function dispatch(action) {
    // action 必须是一个普通对象
    if (!isPlainObject(action)) {
      throw new Error('balabala')
    }

    if (typeof action.type === 'undefined') {
      throw new Error('balabala')
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 执行 reducer 对 store 进行修改
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 结合 subscribe 中的相关代码,可以看到为了保证在对 listener 的一次遍历过程中,
    // 其不会因为 listener 内部包含了订阅/取消订阅而使得调用顺序变得一团糟,执行过程中
    // 所有对于 listeners 数组的操作均在下一次 dispatch 中才生效。
    // 为此,使用两个 listener 数组是必须的。
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 对当前的 reducer 进行替换,以满足动态修改 reducer 的需求
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 提供与响应式库协同的入口
  function observable() {
    // balabala...
  }

  // 当 store 被创建时,触发 init 事件从而允许 reducer 执行一些初始化操作
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

createStore函数中的逻辑比较简单,基本上就是依次创建各个API函数并包裹起来返回。

需要注意其中的nextListeners以及currentListeners两个内部变量,如果仅仅为了存储 listener 并对其执行调用的话,只需要一个数组就足够了,但是这里却使用了两个数组。事实上,listener 内部可能包含了 subscribe 或者 unsubscribe 的操作,仅在一个数组上操作可能会导致在一次 listener 的遍历过程中,其执行顺序变得一团糟。添加 nextListeners数组后,listener 中的订阅/取消订阅操作仅在 nextListeners上执行,并且在下一次遍历 listener 的操作前才生效,从而保证了执行过程的有序。

不过对于createStore函数我仍存在一些疑问,对于 reducer参数的判断放在了enhancer执行之后,这样使得程序进入了enhancer忙活了一堆之后才发现基本的reducer还存在问题。虽然对于正确性和性能都没什么问题,但是终归看起来觉得不优雅。

bindActionCreators

bindActionCreators函数用于多个 actionCreator 绑定到 dispatch 上使得调用更为便捷。

先分析绑定单个 action 到 dispatch 上的函数bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

如果将其中的 actionCreator参数用 f替代,dispatch参数用 g替代并做一些简化可以看到:

function bindActionCreator(f, g) {
	return (...args) => { f(g(...args)) }
}

其实bindActionCreator就是对于 actionCreator 和 dispatch 两个函数的 compose。
对应的,bindActionCreators也相类似:

export default function bindActionCreators(actionCreators, dispatch) {
  // 单一函数直接调用 bindActionCreator ,不过这会导致函数返回结果类型的不一致
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

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

  // map actionCreators数组,并分别做 compose,最终返回一个操作后的数组
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    // 仅对类型为 function 的 actionCreator 进行处理
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

具体实现没什么可说的,不过有一点要吐槽:bindActionCreators针对于传入参数的类型的不同,其返回值也会随之改变。当传入的actionCreators为一个函数时,返回值为函数;传入的actionCreators为函数的数组时,返回值也是一个数组。这样的设计可能会导致草草看了一眼API文档的开发者掉进了返回值类型不同的坑里头去。

combineReducers

combineReducers函数位于 combineReducers.js 文件中,里面同时还包含了一些验证 reducer 处理结果的相关函数。
combineReducers函数用于对多个 reducer 进行整合生成一个的 reducer,直接来看代码:

// 对多个 reducer 进行整合
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍历 reducers 并将属于 function 的元素放入 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 是否能够处理 init 事件以及未知事件
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    // 开发环境中预先对 reducer 处理结果进行判断并报 warning
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,finalReducers,action,unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    // 遍历 finalReducer 并对应生成 stateForKey 并放入 nextState 中。
    // preloadedState 中 key 不被包含在 finalReducer 中的 stateForKey 就被被抛弃了。
    // 不过问题也不大,根本不会被 reduce 所操作的数据本身就应该在配置文件中写死。
    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函数的逻辑很简单,也没有使用 compose 等方法,直接将多个 reducer 保存在对象中返回一个函数用于对其进行遍历操作。
其中需要注意的是,由于combineReducers所返回的函数处理逻辑,其不会在遍历开始之前就拷贝一份原先状态,而是根据 reducer 对应的 key 来依次添加 stateForKey 的数据。这也就意味着,preloadedState 的设定的初始 state 中的部分数据,如果其 key 没有对应到某个的 reducer,则会导致其在更新过程中丢失。不过从设计的合理性角度来讲,这一特性也不成问题,毕竟本身不对应到 reducer 的数据意味着其不可更改,应当在配置文件中写死。

★ applyMiddleware

(敲黑板:重点内容,大家拿出笔记一下)
最后是重头戏applyMiddleware函数。这也是 Redux 允许开发人员为其应用插件的核心 API 。applyMiddleware函数代码实现部分短小精悍,仅有 20 余行,但是理解起来还需稍微费些功夫。

为了理解applyMiddleware函数的实现原理,首先应当了解其应用场景:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
applyMiddleware(logger)

上述代码中展示了一个最简单的单一插件应用实例。可以看到,插件是一个较为复杂的高阶函数。这里依次对其传入的参数为storenextaction
接下来,先转过头来看applyMiddleware函数的实现:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 开头还是老样子,先把 store 按默认方法创建出来
    const store = createStore(...args)
    // 创建一个 dispatch 的临时版本,其在最终完成 dispatch 构建之前被调用则会抛出错误
    let dispatch = () => {
      throw new Error('balabala')
    }

    // 提供一个伪造的 store,包含了中间件可能用到的函数调用
    const middlewareAPI = {
      getState: store.getState,
      // 这里的 dispatch 包裹了 compose(...chain)(store.dispatch) 所生成的 dispatch,
      // 其用于再次遍历整个 middleware 链,对异步的 middleware 非常有用。
      dispatch: (...args) => dispatch(...args)
    }
    // 注意这里的 chain 就是个数组的意思,和函数式编程的 chain 概念没有关联
    // 将伪造的 store 绑定到中间件的闭包里,map 后生成中间件绑定后的数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 将所有的函数进行 compose 并将 dispatch 作为 next 绑定到闭包中,生成一个新的 dispatch。
    // 实际的执行过程中,dispatch 将在最后一个中间件执行 next(action) 时被调用,
    // 但是其结果会被一层层的回传到当前中间件位置。
    // 中间件的执行顺序应该是:applyMiddleware(f, g) --> g -> f -> dispatch
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

看完注释可能你还是会觉得糊里糊涂,不打紧,我再对整个代码实现思路做一次详细的描述。
还记得createStore函数中调用applyMiddleware执行结果的方式吗:

enhancer(createStore)(reducer, preloadedState)

可以看出,applyMiddleware函数实际返回的是一个以createStore为参数的加强版createStore函数,其对于dispatch 扩充了中间件所提供的功能。
结合使用示例以及源码,现在可以对中间件所使用的参数进行解读:

  • store参数实际上传入的是 middlewareAPI。其是一个伪造的 store,提供了类似于getState以及dispatch两个函数。
  • next参数则是指向其前一个中间件生成的函数
  • action参数则是强后的 dispatch函数被调用时传入的 action。

具体而言:
传入storemiddlewareAPI中的dispatch函数事实上是使用了箭头函数对外部的dispatch进行了一层包裹。这一设计的目的是为了防止中间件直接对dispatch进行修改从而导致错误,同时也使得外部的dispatch需要被预先声明。为了防止dispatch在构建完成前被调用,在声明时为其赋值一个调用即报错的函数。

applyMiddleware构建生成dispatch函数的核心代码就两句:

    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

第一行代码中,middleware 数组进行 map 操作,将middlewareAPI保存在中间件的闭包中。此时,被调用一次后的中间件变为了:

next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

第二行代码中,保存了middlewareAPI的中间件进行 compose 并直接传入 store.dispatch执行。这一行的执行用代码表示如下:

// 假设 f 和 g 均为保存了 middlewareAPI 后的中间件
let f = next => action => {
  console.log('middle f')
  return next(action)
}
let g = next => action => {
  console.log('middle g')
  return next(action)
}

// 将 f 和 g 进行组合的结果为:
compose(f, g) --> (...args) => f(g(...args)) --> 
((next) => (action => { 
  console.log('middle g')
  return next(action)
})) => (action => {
  console.log('middle f')
  return next(action)
})
// 上述并非严格的代码,其表明了一个执行逻辑:先执行中间件 g 中的函数,后将其作为参数 next 传入 f 中执行

// 传入 store.dispatch 调用的结果为:
compose(f, g)(store.dispatch) --> f(g(store.dispatch)) -->
action => {
  console.log('middle f')
  return ((action => { 
    console.log('middle g')
    return store.dispatch(action)
  }))(action)
}
// 由于传入了 store.dispatch 参数并执行,所有的 next 参数都被传入函数内,并返回以 action 为参数的最底层函数

如上述代码所示, f中间件的 next 为根据 g 中间件生成的函数;g中间件的 next 为 store.dispatch。如果还有不清楚的地方,就结合代码跑一跑看看罢,实践出真知嘛。

需要注意的是,实际上通过applyMiddleware函数所加强的dispatch函数,其对中间件的执行顺序是从右到左的。仍以上述代码为例,传入中间件顺序是:f, g,但是实际执行完毕的顺序是:dispatch -> g -> f。其原因为:传入store.dispatch参数并执行的过程将函数 compose 后得到的头衔尾的参数传递关系解开了,从而暴露出了最外层的返回的以action为参数的类 dispatch 函数。

总结

做个简单的总结吧。
Redux 代码本身比较短小精炼,不像上万行的 React 代码看得人晕头转向。代码虽然简单,但是其背后的设计思想才是其最具价值的内容。此外,例如applyMiddleware极简的实现也同样值得好好学习。

// 磨蹭了个把月才划掉日程表上的一项,我可得长点心了。(点心,什么点心)

你可能感兴趣的:(前端杂物筐)