在做这个项目的过程中,hooks 的 api 相当简洁,代码也容易理解,但 Redux 就不一样了,大量的样板代码,以及各种纯函数的限制,让刚刚上手的新人总会 感觉有些不适应。React 的开发,很大一部分的门槛在于 Redux。可能有人会说了,都 9012 年了,还用什么 Redux 管理数据啊,直接 hooks 一把撸。对于这些人的观点,我认为 Redux 由于出色的调试机制和完整的模块管理功能,是一个短时间不可被替代的状态管理方案。

因此我觉得我们在熟练使用 Redux 的同时,也有必要去研究它内部的原理,体会它的设计思想,这样不仅仅能够加深我们对于 Redux 本身的理解,也能够巩固原生 JS 的 功底,锤炼我们的编程思想。

还记得这些熟悉的代码吗?

import {createStore, compose, applyMiddleware} from "redux";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore (reducer, composeEnhancers (applyMiddleware (thunk)));
import { combineReducers } from "redux-immutable";
import { reducer as recommendReducer } from "../application/Recommend/store/index";
//...

export default combineReducers ({
  recommend: recommendReducer,
  //...
});

你知道 createStore 发生了什么?dispatch 执行后在内部怎么运作的?compose 函数做了什么事情?combineReducers 是如何合并不同的 reducer 的?applyMiddleware 是如何组织中间件的?

接下来我们就来一一拆解 Redux 在背后为我们做的这些事情。

首先要声明的是,为了把原理讲清楚,不可避免地会涉及到源码,但是源码有大量的类型判断和边界检查,如果一一列举,其一对我们理解 Redux 本身的原理没有帮助,其二分散我们的注意力、 浪费大量时间。因此凡是对我们理解 Redux 原理没有帮助的源码部分,我们不予考虑,如果真的很感兴趣可以 GitHub 仓库 (opens new window) 下载它的源码自己去看。

# createStore 揭秘

createStore,顾名思义,是要创建一个仓库,是 redux 的核心所在, 它最后要返回四个非常重要的属性,分别是 getState,subscribe,dispatch,replaceReducer。

export default function createStore (reducer, preloadedState, enhancer) {
  //...
  return {
    getState,// 获取到 state
    subscribe,// 采用发布订阅模式,这个方法进行观察者的订阅
    dispatch,// 派发 action
    replaceReducer// 用新的 reducer 替换现在的
  }
}

进入 createStore,第一步是检查参数,一共可以接收三个参数,reducer 表示改变 store 数据的纯函数,preloadedState 表示初始状态,第三个参数暂且不管,后面讲到中间件机制你就 明白它的用意了。

export default function createStore (reducer, preloadedState, enhancer) {
  //reducer 必须是函数
  // 当前 reducer
  let currentReducer = reducer
  //state 数据,redux 的根本
  let currentState = preloadedState
  // 订阅者集合
  let currentListeners = []
  // 虽然不起眼,但是是一个关键的设计
  let nextListeners = currentListeners
  // 是否正在有 dispatch 在运行
  let isDispatching = false

  //...
  //return 代码
}

首先看看它的 getState 方法:

function getState () {
  // 如果有 dispatch 正在执行则报错
  if (isDispatching) throw new Error ("xxxx 具体信息省略")
  return currentState
}

它的 subscribe 方法其实是基于发布订阅模式的,我们想一想只有一个数组来存放订阅者的时候可能会出现什么问题。

假若有十个订阅者订阅了 store, 然后一旦条件触发 store 会依次执行所有的订阅者 (注意这里的订阅者 listener 都是方法,下面代码中的类型判断里面有提)。

这个时候第一个方法中干了一件特别 "孙子" 的事情,它把其他 9 个人全部退订了。那这个时候数组里面只剩下 1 个订阅者,但是循环还在继续啊,从数组后面的索引拿订阅者来执行,会报错,因为 已经不存在了。

当然还有更加复杂的情况,这些情况本质上是订阅者 (可以认为函数) 拥有订阅和退订的权利,也就是说,它可以改变订阅者数组。但是我们遍历订阅者的时候是基于最开始的那个订阅者数组。

因此我们需要缓存最开始的数组,在调用订阅者的时候,一切关于 currentListeners 的改变都不允许,但是可以拷贝一份同样的数组,让它来承担订阅者对数组的改变,那这个数组就是 nextListeners。

subscribe 方法如下定义:

function ensureCanMutateNextListeners () {
  // 如果 next 和 current 数组是一个引用,那这种情况是危险的,原因上面已经谈到,我们需要 next 和 current 保持各自独立
  if (nextListeners === currentListeners) {
    nextListeners = currentListeners.slice ()
  }
}

function subscribe (listener) {
  if (typeof listener !== 'function') {
    throw new Error ('Expected the listener to be a function.')
  }
  // 如果正在有 dispatch 执行则报错
  if (isDispatching) {
    throw new Error ("xxx")
  }
  let isSubscribed = true
  ensureCanMutateNextListeners ()
  nextListeners.push (listener)
  // 返回的是一个退订的方法,将特定的 listener 从订阅者集合中删除
  return function unsubscribe () {
    // 已经退订了就不管了
    if (!isSubscribed) return;
    if (isDispatching) throw new Error ("xxx 具体信息省略")

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

值得注意的是每次调用这个函数的时候,都会产生一个闭包,里面存储着 isSubscribed 的值,调用 n 次就会产生 n 个这样的闭包,用来存储 n 个不同的订阅情况。 仔细想想还是比较巧妙的做法。

接下来是 dispatch 函数:

function dispatch (action) {
  //action 必须是一个对象
  //action.type 不能为 undefined

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

  try {
    isDispatching = true
    // 看到没有?执行 reducer 后返回的状态直接成为 currentState 了
    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
}

接下来是 replaceReducer:

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

  currentReducer = nextReducer
  // 此时无法匹配任何的 action,但是返回的状态可以将 currentState 给更新
  // 也就是更新当前的 state,因为 reducer 更新了,老的 state 该换了!
  dispatch ({ type: ActionTypes.REPLACE })
}

# combineReducer 做了些什么?

还记得我们怎么使用 combineReducer 的吗?

import { combineReducers } from "redux-immutable";
import { reducer as recommendReducer } from "../application/Recommend/store/index";
import { reducer as singersReducer } from "../application/Singers/store/index";

export default combineReducers ({
  recommend: recommendReducer,
  singers: singersReducer,
});

combineReducer 用来组织不同模块的 reducer,那背后是怎么组织起来的呢?除去容错性的代码,我们看看 combineReducer 的核心源代码:

export default function combineReducers (reducers) {
  // 以项目中的例子来讲,reducerKeys 就是 ['recommend', 'singers']
  const reducerKeys = Object.keys (reducers)
  //finalReducers 是 reducers 过滤后的结果
  // 确保 finalReducers 里面每一个键对应的值都是函数
  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) {
    // 这个标志位记录初始的 state 是否和经过 reducer 后是一个引用,如果不是则 state 被改变了
    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys [i]
      const reducer = finalReducers [key]
      // 原来的状态树中 key 对应的值
      const previousStateForKey = state [key]
      // 调用 reducer 函数,获得该 key 值对应的新状态
      const nextStateForKey = reducer (previousStateForKey, action)
      nextState [key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 这个很简单理解吧?如果没改变直接把原始的 state 返回即可
    return hasChanged ? nextState : state
  }
}

很简单理解吧?好,我们现在进入最硬核的部分!

# compose 函数解读

compose 其实是一个工具,充分体现了高阶函数的技巧。源码如下:

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)))
}

举个例子:

const f0 = (x) => { console.log (x) }
const f1 = () => { console.log (1) }
const f2 = () => { console.log (2) }
let fArr = [f2, f1, f0];
console.log (compose (...fArr)(100)) // 执行 f2 (f1 (f0 (100))) 输出 100 1 2

现在先埋下伏笔。之后在 applyMiddleware 中如何大显身手。

# applyMiddleware 完全解析

这个方法与中间件息息相关,一上来就干讲是很不容易理解的,现在我们以项目中用到的 redux-thunk 中间件为例来演示,先放出 redux-thunk 的源码 (你没看错,就这么一点儿):

function createThunkMiddleware (extraArgument) {
  // 这里将 middlewareAPI 给解构成了 { dispatch, getState }
  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;

现在我们来打开 applyMiddleware 的源代码:

export default 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.'
      )
    }
    //middlewareAPI 其实就是拿到 store 的信息
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch (...args)
    }
    // 参考上面的 thunk,其实就是传入 store 参数,剩下的部分为 next => action => { ... };
    // 传入这个参数是必须的,因为需要拿到 store 的相关属性,如 thunk 拿了 getState
    // 这里的意思就是每个中间件都能拿到 store 的数据
    const chain = middlewares.map (middleware => middleware (middlewareAPI))
    dispatch = compose (...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

加入现在还有一个 redux-logger 的中间件,调用 applyMiddleware (logger, thunk), 那么走到 compose 逻辑的时候,相当于 调用 logger (thunk (store.dispatch))。这样就完成了中间件的机制。仔细体会一下这中间的执行顺序,其实并不难。

# 探究 createStore 留下来的问题

刚刚在 createStore 那一段提了下参数类型判断,但是第三个参数没有展开讲,那这里面究竟是如何来判断的呢?现在我觉得时机成熟了。

给出这一部分源代码:

export default function createStore (reducer, preloadedState, enhancer) {
  // 第二个参数为函数,但是第三个参数没传
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState  // 将第二个参数当做 enhancer
    preloadedState = undefined
  }
  // 确保 enhancer 为函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error ('Expected the enhancer to be a function.')
    }

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

判断类型后返回 enhancer (...) 是针对什么样的场景的呢?

如果要用 thunk 中间件,那么 redux 官方文档是这么写的:

const store = createStore (reducer, applyMiddleware (thunk));

看到没?这个时候其实 redux 内部的 enhancer 就变成了 applyMiddleware (thunk) 的结果。

运行流程其实变成了 applyMiddleware (thunk)(createStore)(reducer, preloadedState);

而返回的结果赋给了 store, 当前 store 中的 dispatch 属性已经成功被更改,一旦走入 dispatch,必然经过中间件。中间件成功地集成!

知道了原理后,相信你再写一个自己的 Redux 中间件也易如反掌了。

function createMyMiddleware (...arg) {
  return ({ dispatch, getState }) => next => action => {
    console.log ("我开发的 Redux 中间件")
    return next (action);
  }
}

const myMiddleware = createMyMiddleware ()

export default myMiddleware;

然后在 createStore 的时候应用:

import thunk from 'react-thunk';
import myMiddleware from 'my-middleware';
const store = createStore (reducer, applyMiddleware (thunk, myMiddleware));

中间件里面具体编写什么内容,应该由业务场景来决定,这里就不展开了。

# Redux 源码中一些有意思的工具函数

# 1. 判断是否为普通的对象

export default function isPlainObject (obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf (proto) !== null) {
    proto = Object.getPrototypeOf (proto)
  }

  return Object.getPrototypeOf (obj) === proto
}

# 2. 生成随机字符串

const randomString = () =>
  Math.random ()
    .toString (36)
    .substring (7)
    .split ('')
    .join ('.')

Redux 原理的解读就到这里了,其实理解它的源码也并没有那些难,但我觉得最重要还是将它的原理和使用结合起来,体会整个设计的思想,研究这些对个人的成长还有是很有帮助的,也希望这篇文章能够起到抛砖引玉的作用,让大家带着更多的好奇和兴趣去研究其他工具的原理,提升自己的思维层次和工程能力。大家加油!

阅读全文