从应用到源码-深入浅出Redux

11,500 阅读31分钟

引言

大家好,这是一篇没有任何注水的 Redux 从入门到精通的源码解读文章。

文章中的每一行代码都是笔者深思熟虑敲下的,欢迎对 Redux 感兴趣的同学共同讨论。

文章篇幅较长,建议收藏逐步阅读。希望文章中的内容可以对大家有所启发。

createStore

基础概念

谈起 redux 首当其冲必须从最开始的 createStore 入口方法谈起,我们先来看看 createStore 的用法。

语法: createStore(reducer, [preloadedState], enhancer)

createStore 通过传入三个参数创建当前应用中的唯一 store 实例对象,注意它是全局唯一单例。

后续我们可以通过 createStore 返回的 dispatch、subscribe、getState 等方法对于 store 中存放的数据进行增删改查。

我们来看看所谓传入的三个参数:

reducer

reducer 作为第一个参数,它必须是一个函数。

相信有过 redux 开发经验的同学,对于 reducer 并不陌生。比如一个常见的 reducer 就像下面这个样子:

import { Reducer } from "redux"

interface NameReducerState {
  name: string
}

interface NameReducerAction {
  type: typeof CHANGE_NAME;
  [payload: string]: string;
}

const initialState = {
  name: ''
}

const CHANGE_NAME = 'change'
export const changeAction = (payload: string) => ({ type: CHANGE_NAME, payload })

const name: Reducer<NameReducerState, NameReducerAction> = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_NAME:
      return { name: action.payload }
    default:
      return state
  }
}

export default name

上边的 name 函数就是一个 reducer 函数,这个函数接受两个参数分别为

  • state 这个参数表示当前 reducer 中旧的状态。

  • action 它表示本次处理的动作,它必须拥有 type 属性。在reducer函数执行时会匹配 action.type 执行相关逻辑(当然,在 action 对象中也可以传递一些额外的属性作为本次reducer执行时的参数)。

需要额外注意的是,在 redux 中要求每个 reducer 函数中匹配到对应的 action 时需要返回一个全新的对象(两个对象拥有完全不同的内存空间地址)。

preloadedState

preloadedState 是一个可选参数,它表示通过 createStore 创建 store 时传递给 store 中的 state 的初始值。

简单来说,默认情况下通过 createStore 不传入 preloadedState 时,当前 store 中的 state 值会是通过传入的 reducer 中第一个参数 initizalState 的默认值来创建的。

比如这样:

function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

// 不传入preloadedState
const store = createStore(reducer)

console.log(store.getState()) // { number: 1 }

// ----此处分割线----


function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer, { number: 100 })

console.log(store.getState()) // { number: 100 }

相信通过上边两个例子大家已经明显能感受到 preloadedState 代表的含义,通过在进行服务端同构应用时这个参数会起到很大的作用。

当然,这个参数与 combineReducers 或多或少存在一些关系。我们会在稍后谈论到。

enhancer

enhancer 直译过来意味增强剂,其实也就是 middleware 的作用。

我们可以利用 enhancer 参数扩展 redux 对于 store 接口的改变,让 redux 支持更多各种各样的功能。

当前,关于 enhancer 在文章的后续我们会详细深入。本质上它仍然通过一组高阶函数(HOC)来拓展现有 redux 中 store 功能的辅助中间件函数。

实现

思路梳理

在上边我们简单介绍了 createStore 基础的用法以及对应的含义,那么此时我们直接上手来实现一下对应的 createStore 函数吧。

输入

首先,createStore 方法会接受三个参数。上边我们分析过分别为 reducer、preloadedState 以及 enhancer 。

关于 enhancer 我们现在暂时先抛开它,后续我们会详细详细就这个点来展开。

输出

createStore 返回的 store 中会返回以下几个方法:

  • dispatch

dispatch 是一个方法,修改 store 中的 state 值的唯一途径就是通过调用 dispatch 方法匹配 dispatch 传入的 type 字段从而执行对应匹配 reducer 函数中的逻辑修改 state 。

比如:

function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer, { number: 100 })

console.log(store.getState()) // { number: 100 }

// ---后续代码会省略上述创建store逻辑-----

// 派发dispatch 修改store
store.dispatch({ type: 'add' })

console.log(store.getState()) // { number: 101 }
  • subscribe

subscribe(listener) 方法会接受一个函数作为参数,每当通过 dispatch 派发 Action 修改 store 中的 state 状态时,subscribe 方法中传入的 callback 会依次执行。

并且在传入的 listener callback 中可以通过 store.getState() 获得修改后最新的 state 。

需要注意的是 subscriptions 在每次进行 dispatch 之前都会针对于旧的 subscriptions 保存一份快照。

这也就意味着当 subscriptions 中某个 subscription 正在执行时去掉 or 订阅新的 subscription 对于当前 dispatch 并不会有任何影响。

  • getState

getState 方法正如它定义的名字一般,它会返回应用当前的 state 树。

  • replaceReducer

replaceReducer 方法接受一个 reducer 作为参数,它会替换 store 当前用来计算 state 的 reducer。

思路

未命名文件.png

整体思路我画了一张草图来给大家提供一些思路,核心其实就是在 createStore 中通过闭包的形式访问内部的 state 从而进行一系列操作。

当然,也许现在对于这张图你会感到疑惑。没关系,稍后我们自己实现完基础的 redux 后在回头来看我相信你会清晰的。

实现

现在我们来稍微实现一般基础版的 redux :

首先我们我们在 src/core 中新建一个 createStore.ts 文件,根据刚才提到的思路我们来填充这个方法:

createStore.ts 基础逻辑

/**
 * 创建Store
 * @param reducer  传入的reducer
 * @param loadedState 初始化的state
 * @returns 
 */
function createStore(reducer,loadedState) {
  // reducer 必须是一个函数
  if (typeof reducer !== 'function') {
    throw new Error(
      `Expected the root reducer to be a function. `
    )
  }

  // 初始化的state
  let currentState = loadedState
  // 初始化的reducer
  let currentReducer = reducer
  // 初始化的listeners
  let currentListeners = []
  // listeners 的快照副本
  let nextListeners = currentListeners
  // 是否正在dispatch
  let isDispatching = false

  /**
   * 派发action触发reducer
   */
  function dispatch(action) {
    
  }

  /**
   * 订阅store中action的变化
   */
  function subscribe() {

  }

  /**
   * 获取State
   */
  function getState() {
    return currentState
  }

  /**
   * 替换store中的reducer
   * @param reducer 需要替换的reducer
   */
  function replaceReducer(reducer) {

  }

  return {
    dispatch,
    replaceReducer,
    getState,
    subscribe
  }
}

export default createStore

我们构造了基础的 createStore 逻辑,仅仅是填充了 getState 方法。

因为这个方法非常简单,它就是获得当前 store 内部中的 currentState 。

dispatch 方法

接下来我们来看看对应的 dispatch 方法:

我们提到过 dispatch 方法会接受一个 action 的参数,通过匹配 action.type 来匹配 reducer 中的分支从而返回一个全新的 State 。

// ...

  /**
   * 派发action触发reducer
   */
  function dispatch(action) {
    // action 必须是一个纯对象
    if (!isPlainObject(action)) {
      throw new Error(`You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
    }

    // action 必须存在一个 type 属性
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
      )
    }

    // 如果当前正在dispatching状态 报错
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 调用reducer传入的currentState和action
      // reducer的返回值赋给currentState
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 当reducer执行完毕后 通知订阅的listeners 依次进行执行
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    // dispatch完成返回action
    return action
  }
// ...

其实可以看到 dispatch 的逻辑非常清晰的,首先 dispatch 函数中进行了参数校验。

传入的action必须是一个对象,并且必须具有 type 属性,同时当前 store 中的 isDispatching 必须为 false 。

当满足边界条件后,首先会将 isDispatching 重置为 true 的状态。

之后调用传入的 currentReducer 函数,传入旧的 state 以及传入的 action 执行 reducer ,将 reducer 中返回的结果重新赋值给 currentState。

其实 dispatch 的逻辑非常简单,完全就是利用闭包的效果。传入 store 内部维护的 currentState 以及传入的 action 作为参数调用 createStore 时传入的 reducer 函数获得返回值更新 currentState 。

同时在 action 执行完毕后,遍历 nextListeners 中订阅的函数,依次执行 nextListeners 中的函数。

subscribe 方法

上边我们提到了,在通过 action 触发 reducer 执行完成后,会依次调用 nextListeners 中的方法。

那么 nextListeners 中的方法是哪里来的呢? 恰恰是通过 createStore 返回的 subscribe 进行订阅的。

它的逻辑非常的简单,相信不少同学已经可以猜出来它是如何实现的。同样也是利用闭包的特性配合发布订阅模式,通过 subscribe 方法传入 listener 进行订阅,在每次 action 派发结束后依次调用订阅的 listener。

 /**
   * 订阅store中action的变化
   */
  function subscribe(listener: () => void) {
    // listener 必须是一个函数
    if (typeof listener !== 'function') {
      throw new Error(
        `Expected the listener to be a function. Instead.`
      )
    }
    // 如果当前正在执行 reducer 过程中,不允许进行额外的订阅
    if (isDispatching) {
      throw new Error()
    }

    // 标记当前listener已经被订阅了
    let isSubscribed = true

    // 确保listeners正确性
    ensureCanMutateNextListeners()
    // 为下一次的listeners中添加传入的listener
    nextListeners.push(listener)

    // 返回取消订阅的函数
    return function unsubscribe() {
      // 如果当前listener已经被取消(未订阅状态,那么之际返回)
      if (!isSubscribed) {
        return
      }
      // 当前如果是reducer正在执行的过程,取消订阅直接报错
      // 换言之,如果在reducer函数执行中调用取消订阅,那么直接报错
      if (isDispatching) {
          // 直接报错
          throw new Error()
      }

      // 标记当前已经取消订阅
      isSubscribed = false

      // 同样确保listeners正确性
      ensureCanMutateNextListeners()
      // 逻辑很简单了利用 indexOf + splice 删除当前订阅的listener
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

上述的逻辑总体比较简单,本质上 subscribe 方法通过操作 nextListeners 数组从而控制订阅的 listeners 。

不过,细心的同学可能会发现对应的 ensureCanMutateNextListeners 并没有实现。我们来看看这个方法究竟是在做了什么:

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

上边在进行分析时,我们提到过:

需要注意的是 subscriptions 在每次进行 dispatch 之前都会针对于旧的 subscriptions 保存一份快照。

这也就意味着当 subscriptions 中某个 subscription 正在执行时去掉 or 订阅新的 subscription 对于当前 dispatch 并不会有任何影响。

这里的 ensureCanMutateNextListeners 恰恰是为了实现这两句中的额外补充逻辑。

在之前我们实现的 dispatch 方法,在 dispatch 触发的 reducer 函数执行完毕后会派发对应的 listeners 依次进行执行。

此时,如果在订阅的 listeners 列表中的 listener 函数再次进行了 store.subscribe 或者调用了已被订阅的 listener 函数的取消订阅方法的话。那么此时并不会立即生效。

所谓不会立即生效的原因就是在这里,在进行 subscribe 时首先会判断 nextListeners === currentListeners 是否相等。

如果相等的话,那么就会对于 nextListeners 进行重新赋值,会将当前 currentListeners 这个数组进行一次浅拷贝。

注意由于 JavaScript 引用类型的关系,此时 nextListeners 已经是一个全新的对象,指向了一个新的内存空间。

而在 dispatch 函数中的 listeners 执行时 :

// dispatch 函数

   // 当reducer执行完毕后 通知订阅的listeners 依次进行执行
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

此时的 listeners 由于引用类型的关系,指针仍然指向旧的(被浅拷贝的原始对象)。所以后续无论是针对于新的 nextListeners 进行添加还是取消订阅,并不会在本轮 dispatch 后的 listeners 中立即生效,而是会在下一次 dispatch 时才会生效。

replaceReducer

接来下我们来看看对应的 replaceReducer 方法,在编写 replaceReducer 方法前我们先来思考一个另外的逻辑。

不知道有没有细心的朋友发现了没有,我们一起来看看这段代码:


function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer)

console.log(store.getState())

此时如果我们没有传递 loadedState 的话,那么,当我们直接调用 store.getState() 按照我们的代码应该返回的 currentState 是 undeinfed 没错吧。

显然这并不是期望的结果,当调用 createStore 时未传入 loadedState 时我们希望 currentState 的值是传入 reducer 函数中第一个参数的默认参数(也就是{number:1})。

那么此时应该如何去处理这个呢,其实答案非常简单。Redux 在 createStore 的函数结尾派发了一次type 为 随机的 action 。

function createStore() {
    // ....
    
    // 派发了一次type为随机的 action ActionTypes.REPLACE其实就是一个随机的字符串
    dispatch({ type: ActionTypes.REPLACE })
    return {
        dispatch,
        replaceReducer,
        getState,
        subscribe
    }
}

同学们可以回忆一下,通常我们在编写 reducer 函数中是否对于匹配的 action.type 存在当任何类型都不匹配 action.type 时,默认会返回传入的 state :

function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

在 createStore 函数中,首次派发了一个 action 并且它的类型不会匹配 reducer 中任何的 actionType。

那么此时调用 reducer ,state 的值会变成默认参数进行初始化。同时在 reducer 执行完成会将返回值赋值给 currentState 。

这样是不是就达到了当没有传入 loadedState 参数时,初始化 currentState 为 reducer 中 state 的默认参数的效果了吗。

当然,如果传入了 loadedState 的话。那么由于第一次派发 action 时任何东西都不会匹配并且传入 reducer 的第一个参数 state 是存在值的(loadedState)。

那么此时,currentState 仍然为 loadedState 。

搞清楚了这个点之后,我们再回到 replaceReducer 的实现上:

  /**
   * 替换store中的reducer
   * @param reducer 需要替换的reducer
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error(
        `Expected the nextReducer to be a function. Instead, received: '}`
      )
    }

    currentReducer = nextReducer

    dispatch({ type: '@redux/INIT$`' })

    return {
      dispatch,
      replaceReducer,
      getState,
      subscribe
    }
  }

replaceReducer 函数我并没有进行逐行注释。其实它的逻辑也非常简单,就是单纯替换 currentReducer 为 nextReducer。

同时派发了一个初始化的 action 。

上述完整的代码仓库你可以在这里看到:代码仓库地址

这个地址我会不定时根据文章更新一些源码解读的相关内容,有兴趣的小伙伴可以 star 关注。

源码分析

其实这里 createStore 的源码就已经没有任何源码分析的必要了。

大家可以看到本身 createStore 做的事情非常简单,通过闭包保存一系列变量返回对应 API 提供给使用方去调用。

完整的源码地址你可以在这里查阅到,我想说的是其实上述实现的代码已经可以说一比一还原了 redux 中 createStore 的源码了。

不过,唯一一些的不同点就是关于一些类型定义 ts 类型的补充,以及 createStore 的返回值源码中会额外多出一个 [$$observable], 日常中对于它的应用可以说是少之又少所以这里忽略了这个方法,当然有兴趣的同学可以自行查阅。

bindActionCreators

基础概念

通常我们在使用 React 的过程中会遇到这么一种情况,父组件中需要将 action creator 往下传递下到另一个组件上。

但是通常我们并不希望子组件中可以察觉到 Redux 的存在,我们更希望子组件中的逻辑更加纯粹并不需要通过dispatch 或 Redux store 传给它 。

也许接触 redux 不多的同学,不太清楚什么是 action creator 。没关系,这非常简单。

const ADD = 'ADD'

// 创建一个ActionCreator
const addAction = () => ({ type: ADD })

function reducer(state = { number: 1 }, action) {
  switch (action.type) {
    case ADD:
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer)


// 通过actionCreator派发一个action
store.dispatch(addAction())

我们将上述的代码经过了简单的修改(其实本质上是一模一样的,只是额外进行了一层包装)。

这里的 addAction 函数就被称为 actionCreator 。所谓的 actionCreator 本质上就是一个函数,通过调用这个函数返回对应的 action 提供给 dispatch 进行调用。

可以明显的看到,如果存在父子组件需要互相传递 actionCreator 时,父传递给子 actionCreator 那么子仍需要通过 store.dispatch 进行调用。

这样在子组件中仍然需要关联 Redux 中的 dispatch 方法进行处理,这显然是不太合理的。

Redux 提供了 bindActionCreators API来帮助我们解决这个问题。

bindActionCreators(actionCreators, dispatch)

参数

bindActionCreators 接受两个必选参数作为入参:

  • actionCreators : 一个 action creator,或者一个 value 是 action creator 的对象。
  • dispatch : 一个由 Store 实例提供的 dispatch 函数。

返回值

它会返回一个与原对象类似的对象,只不过这个对象的 value 都是会直接 dispatch 原 action creator 返回的结果的函数。如果传入一个单独的函数作为 actionCreators,那么返回的结果也是一个单独的函数。

用法

它的用法非常简单,结合上边的代码我们来看看如何使用 bindActionCreators:

import { createStore, bindActionCreators } from 'redux'

// ... 上述原本的 DEMO 示例

// 传入的addAction是一个原始的 actionAcreate 函数,同时传入store.dispatch
const action = bindActionCreators(addAction, store.dispatch)

// 调用返回的函数相当于 => store.dispatch(addAction())
action()
// 同时也支持第一个参数传入一个对象
const action = bindActionCreators({
  add: addAction
}, store.dispatch)

// 通过 action.add 调用相当于 => store.dispatch(addAction())
action.add()

实现

上述我们聊了聊 bindActionCreators 的基础概念和用法,经过了 createStore 的实现后这里我希望同学们可以停下阅读来思考一下换做是你会如何实现 bindActionCreators 。


具体的思路图这里我就不进行描绘了,因为这个 API 其实非常简单。

function bindActionCreators(actionCreators, dispatch) {
  // 如果传入的是函数
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 保证传入的除了函数以外只能是一个Object
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function`)
  }

  // 定义最终返回的对象
  const boundActionCreators = {}

  // 迭代actionCreators对象
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    // 如果value是函数,那么为boundActionCreators[key]赋值
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
}

/**
 * 单个 actionCreator 执行的逻辑
 * @param actionCreator 
 * @param dispatch 
 * @returns 
 */
function bindActionCreator(
  actionCreator,
  dispatch
) {
  return function (this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}

export default bindActionCreators

可以看到 bindActionCreators 函数实现的逻辑非常简单。

如果传入的 actionCreator 是一个函数,那么它会返回利用 bindActionCreator 的新函数。新函数内部同样利用闭包调用了 dispatch(actionCreator.apply(this, args)) 从而达到 派发 action(actionCreator(args)) 的效果。

如果传入的是对象,那么将会返回一个对象。对于对象中的 key 对应的每个 value 会利用 bindActionCreator 函数去处理。

上述完整的代码仓库你可以在这里看到:代码仓库地址

源码解读

同样,原始 Redux 中的源码地址你可以在这里看到

同样,针对于 bindActionCreator 这个函数上述我们实现的逻辑其实是和源码中是一模一样的。我就不过多深入源码解读了。

combineReducers

随着前端应用越来越复杂,使用单个 Reducer 的方式管理全局 store 会让整个应用状态树显得非常臃肿且难以阅读。

此时,Redux API 提供了 combineReducers 来为我们解决这个问题。

概念

combineReducers(reducers)

combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个合成后的 reducer 调用 createStore 方法。

合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。 由 combineReducers() 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名

老样子,我们先来看看 combineReducers 怎么用:

import { combineReducers, createStore } from 'redux'

// 子reduer
function counter(state = {number:1},action) {
    switch (action.type) {
        case 'add':
            return { number: state.number + 1 }
        default:
            return state;
    }
}

// 子reducer
function name(state = { name:'wang.haoyu'},action) {
    switch (action.type) {
        case 'changeName':
            return {name: action.payload}            
        default:
            return state;
    }
}

// combineReducers 合并子reduer为总reducer
const rootReducer = combineReducers({
    counter,
    name
})

const store = createStore(rootReducer)

// { name: { name:'wang.haoyu' }, counter: { number:1 } }
store.getState()

store.dispatch({type: 'add'})

store.dispatch({type: 'changeName', payload: '19qingfeng'})

// { name: { name:'wang.haoyu' }, counter: { number:1 } }
store.getState()

上述代码简单列举了 combinReducers 的使用方法,它的用法也非常简单本质上就是合并传入的 reducer Object 为一个 rootReducer 。

实现

思路分析

回归树.png

针对于上边的 Demo 代码我绘制了一张简单的流程图。

本质上 combinReducers 还是通过传入的 reducerObject 创建了一层嵌套的 object 。

之后在 dispatch 过程中依次去寻找所有的 reducer 进行逻辑调用,最终 getState 返回一个名为 rootState 的顶级对象。

需要留意的一点是在通过 dispatch 触发 action 时多个 reducer 之间我刻意使用了流通这个关键字是有原因的,我们会在稍微详细解释到。

代码实现

分析完大概的实现思路后,我们来一步一步来尝试实现它吧。

首先,我们清楚通过 combineReducers(reducerObject) 最终会返回的是可以传递给 createStore 使用的函数。

那么不难想到 combineReducers 的基础结构如下:


/**
 * combineReducers 接受一个 reducers 结合的对象
 * @param reducers 
 * @returns 返回combination函数 它是一个组合而来的reducer函数
 */
function combineReducers(reducers) {
  // do something
  return function combination(state, action) {
    // do something
  }
}

export default combineReducers

之后我们来一步一步实现所谓的 combineReducers 逻辑。

/**
 * combineReducers 接受一个 reducers 结合的对象
 * @param reducers 传入的 reducers 是一个 Object 类型,同时 Object 中 key 为对应的 reducer 名称,value 为对应的 reducer
 * @returns 返回combination函数 它是一个组合而来的reducer函数
 */
function combineReducers(reducers) {

  // 获得 reducers 所有 key 组成的列表
  const finalReducers = Object.keys(reducers)
  
  return function combination(state, action) {
    // 定义hasChanged变量表示本次派发action是否修改了state
    let hasChanged = false

    // 定义本次reducer执行 返回的整体store
    const nextState = {}

    // 迭代reducers中的每个reducer
    for (let key of finalReducers) {
      // 获得reducers中当前的reducer
      const reducer = finalReducers[key]
      // 获取当前reducers中对应的state
      const previousStateForKey = state[key]
      // 执行 reducer 传入旧的 state 以及 action 获得执行后的 state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 更新
      nextState[key] = nextStateForKey
      // 判断是否改变 如果该reducer中返回了全新的state 那么重制hasChanged状态为true
      hasChanged = hasChanged || nextStateForKey === previousStateForKey
    }

    // 同时最后在根据 finalReducers 的长度进行一次判断(是否有新增reducer执行为state添加了新的key&value)
    hasChanged =
      hasChanged || finalReducers.length !== Object.keys(state).length

    // 通过 hasChanged 标记位 判断是否改变并且返回对应的state
    return hasChanged ? nextState : state
  }
}

上述的代码,我在每一行中都进行了详细的注释。

本质上仍然是通过内部保存传入的 reducers 变量,返回一个整体组装而成的 reducer 函数。

当每次调用 dispatch(action) 时,会触发返回的 combination 函数,而 combination 函数由于闭包会拿到记录的 reducers 对象。

所以当 combination 被调用时非常简单,它拥有 store 中传入的整体 state 状态,同时也可以通过闭包拿到对应的 reducers 集合。自然内部只需要遍历 reducers 中每一个 reducer 并且传入对应的 state 获得它的返回值更新对应 rootState 即可。

这里需要额外注意的是上边我们强调所谓的流通

细心的同学也许会发现一个问题,当我们利用 combineReducers 合并了多个 reducer 后。

当我们派发任意一个 action 时,即使当前派发的 action 已经匹配到了对应的 reducer 并且执行完毕后。

此时剩余的 reducer 函数并不会意味会被中止,相反剩余 reducer 仍然也会传入本次 action 进行继续匹配。

这也就意味着如果不同的 reducer 中存在相同的 action.type 的匹配那么派发 action 时所以匹配到类型的 reducer 都会被计算。

也许,你不是很明白上边那段话。没关系,我们来结合一个简单的例子来看看。

function counter(state = { number: 1 }, action) {
  switch (action.type) {
    case 'add':
      return { number: state.number + 1 }
    default:
      return state
  }
}

function name(state = { name: 'wang.haoyu' }, action) {
  switch (action.type) {
    case 'add':
      return { number: '19QIngfeng' }
    default:
      return state
  }
}
const store = createStore(combineReducers({
  name,
  counter
}))

// 派发一个同名的 action, counter Reducer 和 name Reducer 中都存在这个 actionType
store.dispatch({ type: 'add' })

store.getState() // { name:  { counter: '19Qingfeng' },  counter: { number:2 }}

通过上述的例子其实可以很好的解释 Redux 中流通这个概念。

简单来说,可以总结为通过 combineReducers 合并多个 reducer 后,触发任意 action 无论如何所有 reducer 函数都会被执行。

你可以在这里看到我们实现的 combineReducers 代码

源码解读

上述其实我们已经实现了 redux 中 combineReducers 中的所有核心逻辑,源码中对于 combineReducers 的逻辑无非是比我们实现的版本增加了一些边界条件的处理。

真实的源码位置你可以在这里看到

首先我们从头开始来看:

image.png

源码中 combinReducers 首先对于传入的 reducers 进行了过滤,仅保留了 reducers 中 value 为函数符合条件的对象,保存为 finalReducerKeys 。

之后对于处理后的 reducers 调用了 assertReducerShape(finalReducers) ,这个方法本质上也是针对于每个 reducer 进行校验而言,要求组成 reducers 的每个 reducer 不可返回 undefined :

image.png

在结束上述两部校验之后,combineReducers 会获得匹配结果的 finalReducerKeys 表示传入的 reducers 对象 keys 组成的集合。

image.png

关于getUnexpectedStateShapeWarningMessage方法本质上仍是在进行边界情况的校验,这里我就不展开带大家一行一行看这个方法了,有兴趣的同学可以自行查阅。

再剩余的逻辑其实和之前我们实现的是一模一样的,至此 combineReducers 的实现以及对应的源码也就告一段落了。

Redux 中间件

为什么需要中间件

其实上边我们针对于 redux 的完整生命流程基本已经讨论完毕了。

不过,在上述的 API 代码中,我们能利用的 reducer 也仅仅只是 redux 的基础功能,简单举个例子。

按照上述的使用过程,当触发某些事件时派发 action 交给 store 之后 store 通过 action 和 旧的 state 触发内部 reducer 最终修改 stroe.state 。

组件内部订阅 store.state 的改变,之后在进行 rerender 看上去都是那么一切自然。

可是,假使我们需要在 store 中处理派发一些异步 Action 又该怎么办呢?显然上述的过程完全是一个同步的过程。

上述源生 redux 的整体流程就好像这样:

image.png

看上去非常简单的一个过程,显然它是不能满足我们上述提到的需求。

Redux 提供了中间件的机制来帮助我们修改 dispatch 的逻辑,从而满足各种不同的应用场景。

中间件是什么

上述我们提到过 Redux 中提供了中间件的机制来扩展更多的应用场景,那么什么是中间件呢?换句话说,所谓的中间件究竟有什么作用。

比如刚才的场景下,某些业务场景下我们需要派发一个异步 Action ,此时我们需要支持传入的 action 是一个函数,这样在函数内部可以自由的进行异步派发 action :

import { createStore } from 'redux'

const ADD = 'add'

const reducer = (state = { number: 1 }, action) => {
  switch (action.type) {
    case ADD:
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer)

// 保存 store.dispatch 方法
const prevDispatch = store.dispatch

// 修改store的dispatch方法让它支持传入的action是函数类型
store.dispatch = (action) => {
  if (typeof action === 'function') {
    // 传入的是函数的话,传入prevDispatch调用传入的函数
    action(prevDispatch)
  } else {
    prevDispatch(action)
  }
}

大家留意上述的代码,虽然上述代码粗暴的直接修改了 store.dispatch 的指向,但是 redux 中间件其实本质思想和它是一致的都是通过闭包访问外部自由变量的形式去包裹原始的 action ,从而返回一个新的 action 。

此时,当我们再次调用 store.dispatch 时你就会发现:


// 定义 actionType 注意它是一个函数
const actionType = (dispatch) => {
  setTimeout(() => {
    dispatch({ type: ADD })
  }, 1000)
}

// 派发函数类型的action 
// 此时我们支持了异步的action派发 1s后state.number会变为 2
store.dispatch(actionType)

上述代码完全是一段可以跑起来的伪代码,之所以拿出来这段代码和大家举例更多的是想通过一个简单的例子来为你阐述 Redux 中间件究竟是在做什么。

实现一款中间件

了解了 Redux Middleware 究竟在做什么之后,我们来看看究竟应该如何实现一款 Middleware。

这里我们以一款正儿八经的异步 middleware 为基础先来看看如何实现 Redux Middleware。

一个 Redux Middleware 必须拥有以下条件:

  • middleware 是一个函数,它接受 Store 的 dispatch 和 getState 函数作为命名参数

  • 并且每个 middleware 会接受一个名为 next 的形参作为参数,它表示下一个 middleware 的 dispatch 方法,并且返回一个接受 Action 的函数。

  • 返回的最后一个函数,这个函数可以直接调用 next(action),我们可以通过调用 next(action) 进入下一个中间件的逻辑,注意当已经进入调用链中最后一个 middleware 时,它会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。

综合上边三点,所谓一个middleware大概的结构如下所示:


/**
 * 异步中间件
 * @param param { getState,dispatch } 每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数
 * @returns 返回一个函数
 */
function thunkMiddleware({ getState, dispatch }) {
  // 返回的 next 参数会在下一个middleware中当中当作dispatch来触发action
  return function (next) {
    // 接受真实传入的action
    return function (action) {
      // do something
    }
  }
}

一个middleware大体的结构如下所示,还是稍微比较绕的。

一个 Redux Middleware 是一个函数,它会返回一个接受 next 参数的函数,next 形参表示上一个 middleware 处理后的 dispatch 方法(就好比我们最开始的伪代码,此时的 next 就是我们修改后的 store.dispatch 方法)。

同时,middleware 返回的函数仍会返回一个函数。该函数才是真正的中间件逻辑,它接受外部 dispatch(action) 中的 action 作为参数。

大多数同学对于这些可能感觉到难以理解,没关系此时我们可以仅考虑一个中间件。在单个中间件的情况下,你完全可以将 next 参数当作原本的 dispatch 方法。


/**
 * 异步中间件
 * @param param { getState,dispatch } 每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数
 * @returns 返回一个函数
 */
function thunkMiddleware({ getState, dispatch }) {
  // 返回的 next 参数会在下一个middleware中当中当作dispatch来触发action
  return function (next) {
    // 接受真实传入的action
    return function (action) {
      // 传入的是函数
      if (typeof action === 'function') {
        // 调用函数,将next(单个中间件情况下它完全等同于store.dispatch)传递给action函数作为参数
        // 修改dispatch函数的返回值为传入函数的返回值
        return action(dispatch, getState)
      }
      // 传入的非函数 返回action
      // 我们之前在createStore中实现过dispatch方法~他会返回传入的action
      return next(action)
    }
  }
}

注意 thunkMiddleware 中接受的 dispatch 是已经经过所有 middleware 修改后的 dispatch 而非原始的 store.dispatch 。

上边我们按照步骤实现了一个简单的 Redux-Thunk 中间件,它支持我们传入的 action 类型为一个函数。此时我们就可以在 Redux 中完美的使用异步 Action 。

来看看如何使用它:

import { applyMiddleware, createStore } from 'redux'
import thunkMiddleware from './middleware'

const ADD = 'add'
const reducer = (state = { number: 1 }, action) => {
  switch (action.type) {
    case ADD:
      return { number: state.number + 1 }
    default:
      return state
  }
}

const store = createStore(reducer, applyMiddleware(thunkMiddleware))

// 定义 actionType 注意它是一个函数
const actionType = () => {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: ADD })
    }, 1000)
  }
}

// 派发函数类型的action
store.dispatch(actionType())


setTimeout(() => {
  console.log(store.getState(), '3')
}, 3000)

注意 createStore 内部进行了函数参数的重载判断,这里我们第二个参数传入 applyMiddleware(thunkMiddleware) 相当于第二个参数 preloaded 传入 undefined 第三个参数传入 applyMiddleware(thunkMiddleware) 。

上边的代码,我们使用了 Redux 提供的 applyMiddleware API 来使用 Thunk 中间件。

我们对于 Action 的类型支持传入一个函数,这个函数会接受 dispatch、getState 作为参数从而可以达到实现异步 dispatch 的逻辑。

其实这里不少同学也许仍然还有有很多疑惑,比如中间件的工作机制以及它是如何在 Redux 内部进行串联的。别着急,这里你仅仅需要搞清楚一个中间件长什么样子就可以了。

applyMiddleware

上边我们在 Redux 中使用中间件的时候在 createStore 中传入了第三个参数,并且使用 applyMiddleware 包裹了它。

首先我们先来看看所谓的 applyMiddleware 究竟是什么。

概念

applyMiddleware(...middleware) 是 Reudx 提供给我们使用自定义 middleware 的拓展推荐 API 。

它提供给了我们利用 middleware 的能力来包装 store 的 dispatch 方法从而实现任何我们想要达到的目的。

同时在 applyMiddleware 内部提供了一种可组合的机制,多个 middleware 可以通过 applyMiddleware 组合到一起使用。

参数

  • ...middleware (arguments): 遵循 Redux middleware API 的函数。每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数。该函数会被传入被称为 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是 ({ getState, dispatch }) => next => action

那么一长段话,其实简单来说就是它接受多个 Middleware ,每个 middleware 需要和我们上边提到的结构一致。

返回值

applyMiddleware 会返回函数。它会返回一个应用了 middleware 后的 store enhancer。

applyMiddleware 总结

applyMiddleware 本质上即使对于 Redux 提供了 middleware 的支持能力,并且同时支持传入多个 middleware ,applyMiddleware 内部会对于传入的 middleware 进行组合。

同时,可以看到 applyMiddleware 通常需要配合 createStore 一起使用。在 createStore 中传递了 applyMiddleware 后即可开启 middleware 的支持。

稍微看到它的源码你就会明白它究竟在做什么,特别简单。

applyMiddleware 源码

image.png

applyMiddleware 源码中的每一行我都已经为它进行了详细的注释,可以清晰的看到 applyMiddleWare 通过传入的参数最终返回的是一个全新的 store 。

此处的 compose 函数就是在做函数组合的事情,之后我们会详细解读它。

在来看看所谓 createStore 中接受的 applyMiddleWare 参数:

image.png

注意此处的当我们在 createStore 中传入了 enhancer 时,他会进行

enhancer(createStore)(reducer,preloadedState)

明显看到我们传入的 applyMiddleWare 即是所谓的 enhancer ,相当于 createStore 返回的是:

applyMiddleWare(createStore)(reducer,preloadedState)

这不恰好是 createStore 返回的是通过 applyMiddleWare 返回新的 store 吗,不过返回的 store 中的 dispatch 方法是通过各个中间件进行了改写。

compose

终于到了所谓的 compose 函数了,接触过函数式编程的小伙伴或多或少都听过 compose 函数的鼎鼎大名。

在 Redux 中集成了所谓的 compose 方法,它的作用非常简单从右到左来组合多个函数

上边我们看到在 applyMiddleWare 源码中使用了 compose 方法来组合多个中间件的逻辑。

接下来我们就来揭开它的面纱。

所谓 compose 其实和 Redux 关系并不是很大,只是 Redux 中利用了这个方法来方便的组合中间件而已。

换句话说所谓 compose 组合的应用场景并不仅仅局限于这里,它本身就是函数式编程中的概念。

开始 compose 之前我们先来定义三个简单的中间件:

const promise = ({ getState, dispatch }) => (next) => action => {
  console.log('promise 中间件')
  next(action)
}

const thunk = ({ getState, dispatch }) => (next) => action => {
  console.log('thunk 中间件')
  next(action)
}

const logger = ({ getState, dispatch }) => (next) => action => {
  console.log('logger 中间件')
  next(action)
}

上述代码中,我们定义了三个非常简单的 Redux 中间件。

上边我们提到过在 applyMiddleWare 内部对于中间件的处理流程:

image.png

可以看到在进行 compose 组装之前首先调用了 middlewares.map(middleware => middleware(middlewareAPI))调用了中间件函数。

所以由此可得所谓的 compose 函数首先传入的 chain 我们可以简化成为:

// 直接省略最外层函数,compose处理时最外层函数已经不存在了
const promise = (next) => action => {
  console.log('promise 中间件')
  next(action)
}

const thunk =  (next) => action => {
  console.log('thunk 中间件')
  next(action)
}

const logger =  (next) => action => {
  console.log('logger 中间件')
  next(action)
}

上述我们简化了对应中间件需要 compose 的代码,注意当我们调用 compose 时比如:

compose(logger,thunk,promise) 中间件的组合顺序是从右往左,换言之在真正派发 dispatch 时中间件的执行顺序应该是相反的,也就是从左往右先执行 logger、thunk最后为promise 最后再到真实 store 中的 disaptch。

function compose(...fns) {
  return (args) => {
    // 逆序执行
    for (let i = fns.length - 1; i >= 0; i--) {
      const fn = fns[i]
      args = fn(args)
    }
    return args;
  };
}

源码中的 compose 是使用 reduce API 实现,这里为了方便大家理解我该写成了 for 循环。

可以看到 compose 的代码还是非常简单的,不过这之中稍微有些绕。

首先在使用 compose 函数时:

const composeFn = compose(logger, thunk, promise);

我们调用了 compose 函数传入 三个中间件函数,compose 函数返回一个函数,这对你来说非常简单对吧。

之后,我们会再次调用 compose 函数返回的 composeFn 并且传入 store 的 dispatch 方法:

// 传入真实的dispatch方法返回处理后的dispatch方法
const dispatch = composeFn(store.dispatch);

注意,重点就在这里了。由于闭包的原因,调用 compose 返回的 composeFn 可以访问到在传入的中间件函数 fns 。

此时在 composeFn 内部对于 [logger,thunk,promise] 使用 for 循环进行了逆序调用。

首先我们调用composeFn(store.dispatch) for 循环中首先会拿到 promise 中间件的函数也就是所谓的:

当我们调用 composeFn(store.dispatch) 会发生如下几件事:

  • 首次调用 composeFn(store.dispatch) 传入 args 为 store.disaptch ,注意这里的 args 表示真实的 store.disaptch 方法。

  • 此时函数开始执行,for 循环首先寻找到 promise middleware ,第一次循环中调用 promise middleware 函数传入 store.dispatch 作为参数(args = store.dispatch),调用结束 for 循环内部修改 args 指向 args = fn(args)

经过 for 循环第一次迭代,此时 args 从 store.disaptch 变成了 promise 中返回的函数(这里我们称为promiseAction 函数)

(action) => {
  console.log('promise 中间件');
  // 注意这里的next就相当于 store.disaptch
  // 由于闭包的原因 我们可以在这个函数调用时访问到 next
  next(action);
}
  • 此时 for 循环进入第二次循环中,fn 变成 thunk 中间件函数,此时调用 thunk middleware 函数。并且传入的 args 参数(此时args指向上一次 promise middleware 处理后的返回函数):
// 调用该函数 next 即使 args ,指向上一次处理后返回的函数
const thunk = (next) => (action) => {
  console.log('thunk 中间件');
  next(action);
};

第二次执行完毕后再次修改 args 的指向,让它指向本次 middleware 返回的函数。

  • 第三次循环中,本质上重复了第二次的过程。修改 args 为本次 loggerMiddleware 返回的函数。

  • 循环结束,最终返回拼接后的 args 函数(此时store.disaptch会被重新赋值为返回的args函数)。

当我们调用 store.dispatch 函数时,又会经历以下步骤:

  • 当我们调用 store.dispatch(action) 时,首先拿到返回的 args 函数,相当于调用 args(action)。

  • 由于返回的 args 是 logger 函数(逆序第一个fn)内部的函数,自然优先执行 logger 返回的函数,也就是会执行:

(action) => {
  console.log('logger 中间件');
  next(action);
}
  • 此时 logger 函数中的 next 相当于上一个中间件的 args ,相当于执行:
(action) => {
  console.log('thunk 中间件');
  next(action);
};
  • 此时又回执行 thunk 中间件的逻辑,同理 logger 中的 next 是上一个中间件传递过来的 args,也就是 promise 中间件返回的函数,那么又会执行这个函数:
(action) => {
  console.log('promise 中间件');
  next(action);
};
  • 当执行完 promise 中间件后,此时再次调用 next(action)。此时这里的 next 函数相当于第一次调用 composeFn 传入的 store.dispatch 也就是 composeFn(store.dispatch) 。

  • 最终沿着里链路,进行一路寻找到真实的 store.dispatch 进行派发 action 。

上述的描述过程可能仍然不是那么容易理解,我会把完整代码放在下面。毕竟 compose 是一个比较抽象的过程,建议不是很清晰这一过程的同学可能自行 debugger 进行调试一下。


function compose(...fns) {
  return (args) => {
    // 逆序执行
    for (let i = fns.length - 1; i >= 0; i--) {
      const fn = fns[i]; 
      args = fn(args);
    }
    return args;
  };
}

const promise = (next) => (action) => {
  console.log('promise 中间件');
  next(action);
};

const thunk = (next) => (action) => {
  console.log('thunk 中间件');
  next(action);
};

const logger = (next) => (action) => {
  console.log('logger 中间件');
  next(action);
};

const composeFn = compose(logger, thunk, promise);
// 这里传入的 () => console.log('dispatch') 是一个模拟store.dispatch的方法
const dispatch = composeFn(() => console.log('dispatch'));
dispatch('123'); // log: logger 中间件 thunk 中间件 promise中间件 dispatch

最后我们再来一起看看 Redux 中 compose 的源码:

image.png

其实在你搞清楚上述我们实现的 compose 后在看看源码中的 compose 会好理解很多。

源码中是利用 reducer 形成一层一层闭包引用参数的关系,从而实现中间件的逻辑调用的。

比如以上述的示例 Demo 为例,源码中的 dispatch 方法最终会变成:

const promise = (next) => (action) => {
  console.log('promise 中间件');
  next(action);
};

const thunk = (next) => (action) => {
  console.log('thunk 中间件');
  next(action);
};

const logger = (next) => (action) => {
  console.log('logger 中间件');
  next(action);
};

const dispatch = logger(thunk(promise(disaptch)))

dispach({ type: 'add' })
// 调用时 第一步首先执行 logger 函数的逻辑,同时 logger 中的参数 next 为 thunk(promise(dispatch))
// 当调用 next(action) 时,相当于调用 thunk(promise(disaptch))(action)
// 依次继续执行 thunk middleware 中的逻辑,同时执行到 thunk 中的 next 时,传递了 action 相当于调用 promise(disaptch)(action)
// 继续进入 promise 中进行执行,由于 promise 中的 next 指向的是 store.disaptch 所以最终 promise 执行会执行 store.dispatch(action) , over。

关于 compose 个人建议同学们有空可以复制上边的代码多 debugger 几次,自然也就搞清楚了。

结尾

文章篇幅比较长,但是总结来看 Redux 系列的所有 API 我都已经带大家过了一遍。

之后,如果有时间的话我也会和大家分享一些 Redux 周边生态的用法和源码,比如一些 react-readux、dva、immutabl 等等相关。

当然,因为之前主要技术方向不是 React 所有对于 React 周边生态也是在逐步上手的过程。希望这篇文章可以帮助到大家,大家加油!