Redux 源码分析

131 阅读12分钟
原文链接: zhuanlan.zhihu.com

1. 前言

为使页面组件和业务逻辑解耦,前端相继涌现出 MVC(Model-View-Controller)、MVP(Model-View-Presenter)、 MVVM(Model-View-ViewModel) 模式。在双向数据流的实现中,同一个 View 可能会触发多个 Model 的更新,并间接引起另一个 View 的刷新,使得状态变更的线索及影响变得错综复杂。redux 延续了 flux 架构,倡导单向数据流模式、不能通过访问器属性修改数据,这样就便于追踪状态变化的线索。

图 1,redux 数据流图

在 redux 的设计中,state 为全局缓存的状态(存储于 Store 中),action 为状态变更的标识,派发特定的 action 将引起指定的 state 变更。

不得不指出,首先,在视图组件的实现上,多个 View 可能会复用相同的 state,因此,在一个 View 中派发的 action 可能会影响另一个 View 的状态,这样的话,状态管理上仍会有错综的线索,并不具备清晰性。其次,redux 以状态变更的动作为着眼点,通过 redux 组织业务逻辑,不如包含数据及其变更动作的 Model 直观。

2. 源起

redux 最初需要聚焦于实现 state = fn(state, action) 函数,用于刷新缓存的状态值。在这个函数中,state 可能包含多个状态属性;action 的职责有两种,其一使用 type 属性标识状态值作何变更,其二携带的附属信息将作为引导状态值变更的数据源。对于多种状态值变更,可采用分治的思想将其简化,即 childState = fn(childState, action)。

let state = {
  todoList: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
};

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter;
  } else {
    return state;
  }
};

function todos(state = [], action) {
  switch (action.type) {
  case 'ADD_TODO':
    return state.concat([{ text: action.text, completed: false }]);
  case 'TOGGLE_TODO':
    return state.map((todo, index) =>
      action.index === index ?
        { text: todo.text, completed: !todo.completed } :
        todo
   )
  default:
    return state;
  }
};

function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
};

const Add_Todo_Action = { type: 'ADD_TODO', text: 'Go to swimming pool' };
const Toggle_Todo_Action = { type: 'TOGGLE_TODO', index: 1 };
const Set_Visibility_Filter_Action = { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' };

todoApp(state, Add_Todo_Action);

通过这份来自官网的代码示例,我们既能瞧见源码作者最初聚焦的视点,又能看见 redux 的一大原则 —— 使用纯函数实现状态更新。

在 redux 中,函数 state = fn(state, action) 被称为 reducer。与 Array.prototype.reduce(arrReducer, result) 方法相同的是,在数组的原型方法中,result 的终值通过递归调用 arrReducer 获得;redux 中的 state 更新也是通过逐个调用 reducer 函数实现。如果我们把采用策略模式分而治之的示例代码转变为采用职责链模式实现,即 childState = fn(childState, action) 替换为 state = fn(state, action) 函数,传入 reducer 中的为全量 state 数据,多个 reducer 构成链式结构,当前一个执行完成后,再执行下一个,这样就更接近于 Array.prototype.reduce 方法的执行机制,我们也就更能看出更新状态的函数为什么会被叫做 reducer 了。

图 2,reducer 工作的两种可能性

在 redux 源码中,串联多个 state 实际采用的是策略模式。与示例代码不同的是,state 状态会以业务模块的组织划分成多个状态管理模块,同一个状态管理模块内部又包含多个状态值。对于前者,redux 提供了 combineReducers 方法,用于复合多个 reducer 函数。对于后者,需要使用者手动复合。

3. 实现

3.1. store

上一节提到了状态转换的机制,这是在 action 被派发以后所执行的动作。这一节我们将串联整个链路,包含 action 怎样被派发,状态值如何作缓存,以及更新等。

针对根据 action 触发 reducer 执行这一命题,我们自然地会想到使用发布-订阅模式加以处理,将 action.type 视为订阅的主题,action 中其余属性作为提供给订阅者的额外信息。然而 redux 的宗旨是使状态变化的线索变得清晰,易于追踪和调试,发布-订阅模式和这一宗旨相悖。因为在发布-订阅模式中,同一主题可以有多个订阅者,也就意味着同一个 action 可以触发多个 reducer,线索就会变得错综。在 redux 的设计中,一个 action 只能触发某个特定的reducer 执行。这样,我们就解释了为什么在 redux 源码中,针对独立状态集的多个子 reducer 可以被复合成一个单一的全局总 reducer(简单的,可以通过 switch 语句实现),用于负责处理全局状态的变更。当 action 被派发时,只需调用缓存的全局总 reducer,就可以实现全局状态的更新。

如果我们把总 reducer 称为 finalReducer,全局状态称为 globalState,派发 action 的过程其实只在于唤起 finalReducer 的执行。在 redux 源码中,无论 finalReducer,还是 globalState,都在 store 中维护。store 的执行机制就如下图所示:

图 3,store 的执行机制

为了实现上述机制,store 将 finalReducer, globalState 实现为缓存数据,并提供 getState, dispatch, replaceReducer 方法。其中,store.getState 用于获取全局缓存的状态值,store.dispatch 用于派发 action,store.replaceReducer 用于替换 finalReducer。在 redux 源码中,store 表现为 createStore 模块,其提供 createStore(reducer, initialState) 函数,用于设置 finalReducer, globalState 的初始值。

从源码中抽出这部分内容,即为如下代码(剔除参数校验):

function createStore(reducer, preloadedState) {

  let currentReducer = reducer
  let currentState = preloadedState

  function getState() {
    return currentState
  }

  function dispatch(action) {
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    currentState = currentReducer(currentState, action)

    return action
  }

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

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    getState,
    replaceReducer
  }
}

从上述代码中,redux 在创建 store 的过程,会派发 action = { type: ActionTypes.INIT },意味着可以在应用初始化过程中更新 state;而 store.replaceReducer 方法的存在通常是为了支持编码时的热加载功能,同时又会派发 action = { type: ActionTypes.REPLACE }。从设计的角度考量源码,这是无需多加关注的细节。

3.2. middleware

图 3 中,我们也能看出,action 经由 dispatch 函数直接交给 finalReducer 函数,middleware 中间件的意义是在 action 传入 dispatch 函数前,对 action 进行转换等特殊处理,功能类似 sevelet 中对请求进行转换、过滤等操作的 filter,或者 koa 中间件。redux 中间件的实现上也采用泛职责链模式,前一个中间件处理完成,交由下一个中间件进行处理。

图 4,中间件转换 action 流程

redux 只能从 dispatch 函数的参数中截取到 action,因此在固有程序插入中间件的机制是通过封装 dispatch 函数来完成的,即函数 newDispatch = middleware(dispatch)。这样,在newDispatch 函数体内,我们就能获得使用者传入的 action。

在多个中间件的串联上,redux 借助 Array.prototype.reduce 方法实现。redux 又将 getState, dispatch 作为参数传入 middleware 中,作为工具函数。

使用 redux 时,编写中间件采用 ({ getState, dispatch }) => dispatch => action => { } 形式。再次申明,参数 { getState, dispatch } 为 redux 中间件机制中传入的辅助函数,参数 dispatch 为本次 action 派发过程中唤起执行的 store.dispatch 方法,其意义就是通过封装该函数获取它的参数 action,参数 action 就是本次实际被派发的 action,中间件实际需要处理的转换对象。

图 5,单个中间件的处理流程
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)))
}

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.`
      )
    }
    let chain = []

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

    return {
      ...store,
      dispatch
    }
  }
}

通过上述代码,我们也能看出,redux 植入中间件机制是通过 applyMiddleware 函数封装 createStore 完成的。在重新构建的 createStore 函数体内,其实现也如上文指出的,就是逐个调用中间件函数,对 store.dispatch 方法进行封装。值得借鉴的是,通过包装函数增强原函数的功能,可以使新功能点无缝地插入到原代码逻辑中。

回过头再看 createStore 模块,我们发现,redux 在 createStore 函数的实现上还有第三个参数 enhancer,其主要目的就是为 applyMiddleware 函数提供一个便捷的接口,enhancer 参数的意义也就在于包装 createStore 函数。这又是一个细节,无需多加关注。

function createStore(reducer, preloadedState, enhancer) {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
}

3.3. listener

以上内容无不与状态更新环节相关联,并没有涉及 store 与视图层 view 怎样完成交互。针对这一命题,redux 采用了发布-订阅模式。实际表现为,当 store 派发一个 action 时(可视为发出一个消息),都会促使监视器 listener 与观察者 observer (可视为消息的接受者)运作其处理逻辑。

在具体实现过程中,监视器 listener 通过 store.subscribe 方法添加到 listeners 缓存队列中;当 action 被派发时,其将被取出执行。对于观察者 observer,首先通过 store.observable 方法获得接口层面的可观察对象,其次调用该可观察对象的 subscribe 方法,将 observer.next 转化为 listener,并添加到 listeners 缓存队列中。这样,当 action 被派发时,无论监视器 listener,还是观察者 observer 的 next 方法都将得到执行。不同的是,listener 为无参执行,observer.next 将以即时的 globalState 作为参数。

图 6,listener, observer 执行流程
function createStore(reducer, preloadedState, enhancer) {

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  function subscribe(listener) {
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [?observable]() {
        return this
      }
    }
  }

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

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  return {
    // ...
    subscribe,
    [?observable]: observable

  }
}

4. 工具函数

redux 提供了三个工具函数,分别是前文已给出的 compose 函数,以及 bindActionCreators, combineReducers 函数。compose 函数不再重复说明。

bindActionCreators 函数的意义在于支持动态配置 action。其实现原理是通过 actionCreator 函数生成 action,再调用 store.dispatch 方法加以派发。

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

function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  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') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

combineReducers 函数的意义在于复合 reducer。其实现过程中校验了初始状态,状态的 key 值是否有与之匹配的 reducer。

代码段 6,combineReducers

function combineReducers(reducers) {
  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 = {}
  }

  let shapeAssertionError
  try {
    // assertReducerShape 校验 reducer 返回初始状态非 undefined,且有兜底 state
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

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

    if (process.env.NODE_ENV !== 'production') {
      // getUnexpectedStateShapeWarningMessage 函数校验 state 初始值和 reducer 各键的匹配程度
      // state 初始值可通过 ActionTypes.INIT 设定或参数注入
      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)
      if (typeof nextStateForKey === 'undefined') {
        // reducer 返回值为 undefined 时,由 getUndefinedStateErrorMessage 函数拼接错误文案
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

5. 其他

为简化 redux 使用过程中的编码量,可参考 dva 设计 redux-model 模块,通过 namespace 复合 constants, actions, reducers 文件。

对于异步请求的 loading 状态,可通过制作 redux-loading-middleware 中间件模块在全局层面作统一处理。

以上两点,以及缓存在 store 中数据的设计(在状态管理模块确定之后,如何高效、稳妥地组织状态数据通常是编写业务代码的重心),介于篇幅和能力的限制,我将不再作阐述。

6. 后记

对我这样半道出家的人来说,阅读源码比如专研一本好书。先从薄处入手,藉由丰富的关联性思想到每个可挖掘的点,视野渐渐变得开阔,纸张上的字句也会渐渐变得厚实。再从厚处着眼,借着内在已储备的知识量,更容易拨开阻碍视线的枝蔓,洞见维系着本质的主干,作者构思的线条也会变得越来越简明。像每一段求索经历,这是一个从薄到厚、再从厚到薄的过程,就中的滋味不乏刑侦、推理的乐趣。但是,阅读源码譬如靠经验增进技艺,对知识的汲取往往流于碎片化。对那些才能稍嫌拙劣、又想一探究竟的人来说,以阅读源码的方式攀升到系统化认知的高度,这当中所需的演绎过程将置那些流行、已成熟的技术体系于不顾,势必会耗费莫大的心力,譬如绕一段未必能达到终点的远路。我认为,科班生有一种高屋建瓴的视角,较之半道出家的人,他们具备更为全面的认知,更容易跳过沿途遭遇的细节,理出解决命题的主要线索。当然,假如有个人以一种谈不上正确的方式探寻 api 或数学公式背后的奥秘,他的动机是值得鼓励的。只是等他回落在简单的哲学中,那就需要一段或长或短的时间了。

吴军博士在《数学之美》中引用了牛顿的一句话,”(人们发觉)真理在形式上从来都是简单的,而不是复杂和含混的。“在阅读这本书的过程中,我既能感受到作者行文简明扼要的美感,又能从作者的描述中体会到简单哲学的分量。因为简单,可以助人在错综的表象中洞悉本质,可以摆脱心理上的弊病,免于将学识敷在脸面上。我想,演绎得越多,仰赖于记忆的成分也越少,深藏在海平面下的设计矿产也越加丰富(在其上方形成的概念可以理解为变动不居的表象)。秉持着对简单哲学的信奉,我开始写作这篇分析 redux 源码的文章。虽然以现有的编程功底,想要使这篇文章脉络清晰、简明,我仍然会有力所不逮的感觉。

总之,这篇文章是逐渐摸索的产物,其中难免错谬与勉强,却是我试图在编程行业中登堂入室的中转站。

7. 参考

[深入 React 技术栈 - 陈屹]

Redux 中文文档