前言
阅读对象: 使用过 redux, 对 redux 实现原理不是很理解的开发者.
在我实习入职培训的时候, 我的前端组长就跟我说过, redux 的核心源码很简洁, 建议我有空去看一下, 提升对 redux 系列的理解.
入职一个多月了, 已经参与了公司的不少项目, redux 也使用了一段时间, 对于 redux 的理解缺一直没有深入, 还停留在 "知道怎么用, 但是不知道其核心原理" 的阶段.
所以就在 github 上拉了 redux 的源码, 看了一会, 发现东西却是不多, 比较简洁.
redux 本身的功能是什么
在项目中, 我们往往不会纯粹的使用 redux, 而是会配合其他的一些工具库提升效率, 比如 react-redux, 让 react 应用使用 redux 更容易, 类似的也有 wepy-redux, 提供给小程序框架 wepy 的工具库.
但是在本文中, 我们讨论的范围就纯粹些, 仅仅讨论 redux 本身.
redux 本身有哪些作用? 我们先来快速的过一下 redux 的核心思想(工作流程):
将状态统一放在一个 state 中, 由 store 来管理这个 state
这个 store 由 reducer 创建, reducer 的作用是接受之前的状态, 返回一个新的状态.
外部改变 state 的唯一方法是通过调用 store 的 dispatch 方法, 触发一个 action, 这个 action 被对应的 reducer 处理, 于是 state 完成更新.
可以通过 subscribe 在 store 上添加一个监听函数, store 中 dispatch 方法被调用时, 会执行这个监听函数.
可以添加中间件(中间件是干什么的我们后面讲)
在这个工作流程中, redux 需要提供的功能是:
创建 store, 即: createStore()
将多个 reducer 合并为一个 reducer, 即: combineReducers()
创建出来的 store 提供 subscribe,dispatch,getState 这些方法.
应用中间件, 即 applyMiddleware()
没错, 就这么多方法, 我们看下 redux 的源码目录:
却是也就这么多, 至于其他的如 compose,bindActionCreators 都是一些工具方法. 下面我们就逐个来看看其源码实现.
你可以在 github 上克隆源码到本地, 我后面的分析你可以参照着源码看.
createStore 的实现
这个函数的大致结构是这样:
- function createStore(reducer, preloadedState, enhancer) {
- if(enhancer 是有效的){ // 这个我们后面再解释, 现在可以先不管
- return enhancer(createStore)(reducer, preloadedState)
- }
- let currentReducer = reducer // 当前 store 中的 reducer
- let currentState = preloadedState // 当前 store 中存储的状态
- let currentListeners = [] // 当前 store 中放置的监听函数
- let nextListeners = currentListeners // 下一次 dispatch 时的监听函数
- // 注意: 当我们新添加一个监听函数时, 只会在下一次 dispatch 的时候生效.
- //...
- // 获取 state
- function getState() {
- //...
- }
- // 添加一个监听函数, 每当 dispatch 被调用的时候都会执行这个监听函数
- function subscribe() {
- //...
- }
- // 触发了一个 action, 因此我们调用 reducer, 得到的新的 state, 并且执行所有添加到 store 中的监听函数.
- function dispatch() {
- //...
- }
- //...
- //dispatch 一个用于初始化的 action, 相当于调用一次 reducer
- // 然后将 reducer 中的子 reducer 的初始值也获取到
- // 详见下面 reducer 的实现.
- return {
- dispatch,
- subscribe,
- getState,
- // 下面两个是主要面向库开发者的方法, 暂时先忽略
- //replaceReducer,
- //observable
- }
- }
复制代码
可以看出, createStore 方法创建了一个 store, 但是并没有直接将这个 store 的状态 state 返回, 而是返回了一系列方法, 外部可以通过这些些方法 (getState) 获取 state, 或者间接地 (通过调用 dispatch) 改变 state.
至于 state 呢, 被存在了闭包中.(不理解闭包的同学可以先去了解一下先)
我们再来详细的看看每个模块是如何实现的(省略了错误处理的代码):
- getState
- function getState() {
- return currentState
- }
复制代码
简单到发指, 其实这很像面向对象编程中封装只读属性的方法, 只提供数据的 getter 方法, 而不直接提供 setter.
- subscribe
- function subscribe(listener) {
- // 添加到监听函数数组
- nextListeners.push(listener)
- let isSubscribe = true // 设置一个标志, 标志该监听器已经订阅了
- // 返回取消订阅的函数, 即从数组中删除该监听函数
- return function unsubscribe() {
- if(!isSubscribe) {
- return // 如果已经取消订阅过了, 直接返回
- }
- isSubscribe = false
- // 从下一轮的监听函数数组 (用于下一次 dispatch) 中删除这个监听器.
- const index = nextListeners.indexOf(listener)
- nextListeners.splice(index, 1)
- }
- }
复制代码
- dispatch
- function dispatch(action) {
- // 调用 reducer, 得到新 state
- currentState = currentReducer(currentState, action);
- // 更新监听数组
- currentListener = nextListener;
- // 调用监听数组中的所有监听函数
- for(let i = 0; i <currentListener.length; i++) {
- const listener = currentListener[i];
- listener();
- }
- }
复制代码
createStore 这个方法的基本功能我们已经实现了, 但是调用 createStore 方法需要提供 reducer, 让我们来思考一下 reducer 的作用.
combineReducers
在理解 combineReducers 之前, 我们先来想想 reducer 的功能: reducer 接受一个旧的状态和一个 action, 当这个 action 被触发的时候, reducer 处理后返回一个新状态.
也就是说 ,reducer 负责状态的管理(或者说更新). 在实际使用中, 我们应用的状态是可以分成很多个模块的, 比如一个典型社交网站的状态可以分为: 用户个人信息, 好友列表, 消息列表等模块. 理论上, 我们可以手动用一个 reducer 去处理所有状态的更新, 但是这样做的话, 我们一个 reducer 函数的逻辑就会太多, 容易产生混乱.
因此我们可以将处理逻辑 (reducer) 也按照模块划分, 每个模块再细分成各个子模块, 这样我们的逻辑就能很清晰的组合起来.
对于我们的这种需求, redux 提供了 combineReducers 方法, 可以把子 reducer 合并成一个总的 reducer.
来看看 redux 源码中 combineReducers 的主要逻辑:
- function combineReducers(reducers) {
- // 先获取传入 reducers 对象的所有 key
- const reducerKeys = Object.keys(reducers)
- const finalReducers = {} // 最后真正有效的 reducer 存在这里
- // 下面从 reducers 中筛选出有效的 reducer
- 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);
- // 这里 assertReducerShape 函数做的事情是:
- // 检查 finalReducer 中的 reducer 接受一个初始 action 或一个未知的 action 时, 是否依旧能够返回有效的值.
- let shapeAssertionError
- try {
- assertReducerShape(finalReducers)
- } catch (e) {
- shapeAssertionError = e
- }
- // 返回合并后的 reducer
- return function combination(state= {}, action){
- // 这里的逻辑是:
- // 取得每个子 reducer 对应得 state, 与 action 一起作为参数给每个子 reducer 执行.
- let hasChanged = false // 标志 state 是否有变化
- let nextState = {}
- for(let i = 0; i < finalReducerKeys.length; i++) {
- // 得到本次循环的子 reducer
- const key = finalReducerKeys[i]
- const reducer = finalReducers[key]
- // 得到该子 reducer 对应的旧状态
- const previousStateForKey = state[key]
- // 调用子 reducer 得到新状态
- const nextStateForKey = reducer(previousStateForKey, action)
- // 存到 nextState 中(总的状态)
- nextState[key] = nextStateForKey
- // 到这里时有一个问题:
- // 就是如果子 reducer 不能处理该 action, 那么会返回 previousStateForKey
- // 也就是旧状态, 当所有状态都没改变时, 我们直接返回之前的 state 就可以了.
- hasChanged = hasChanged || previousStateForKey !== nextStateForKey
- }
- return hasChanged ? nextState : state
- }
- }
复制代码
为什么需要中间件
在 redux 的设计思想中, reducer 应该是一个纯函数
维基百科关于纯函数的定义:
在程序设计 https://zh.wikipedia.org/wiki/程序设计 中, 若一个函数 https://zh.wikipedia.org/wiki/子程序#函數 符合以下要求, 则它可能被认为是纯函数:
此函数在相同的输入值时, 需产生相同的输出. 函数的输出和输入值以外的其他隐藏信息或状态 https://zh.wikipedia.org/w/index.php?title=程式狀態&action=edit&redlink=1 无关, 也和由 https://zh.wikipedia.org/wiki/I/O 设备产生的外部输出无关.
该函数不能有语义上可观察的函数副作用 https://zh.wikipedia.org/wiki/函数副作用 , 诸如 "触发事件", 使输出设备输出, 或更改输出值以外物件的内容等.
纯函数的输出可以不用和所有的输入值有关, 甚至可以和所有的输入值都无关. 但纯函数的输出不能和输入值以外的任何资讯有关. 纯函数可以传回多个输出值, 但上述的原则需针对所有输出值都要成立. 若引数是传引用调用 https://zh.wikipedia.org/wiki/求值策略#传引用调用 , 若有对参数物件的更改, 就会影响函数以外物件的内容, 因此就不是纯函数.
总结一下, 纯函数的重点在于:
相同的输入产生相同的输出(不能在内部使用 Math.random,Date.now 这些方法影响输出)
输出不能和输入值以外的任何东西有关(不能调用 API 获得其他数据)
函数内部不能影响函数外部的任何东西(不能直接改变传入的引用变量), 即不会突变
reducer 为什么要求使用纯函数, 文档里也有提到, 总结下来有这几点:
state 是根据 reducer 创建出来的, 所以 reducer 是和 state 紧密相关的, 对于 state, 我们有时候需要有一些需求(比如打印每一阶段的 state).
纯函数更易于调试
比如我们调试时希望 action 和对应的新旧 state 能够被打印出来, 如果新 state 是在旧 state 上修改的, 即使用同一个引用, 那么就不能打印出新旧两种状态了.
如果函数的输出具有随机性, 或者依赖外部的任何东西, 都会让我们调试时很难定位问题.
如果不使用纯函数, 那么在比较新旧状态对应的两个对象时, 我们就不得不深比较了, 深比较是非常浪费性能的. 相反的, 如果对于所有可能被修改的对象(比如 reducer 被调用了一次, 传入的 state 就可能被改变), 我们都新建一个对象并赋值, 两个对象有不同的地址. 那么浅比较就可以了.
至此, 我们已经知道了, reducer 是一个纯函数, 那么如果我们在应用中确实需要处理一些副作用(比如异步处理, 调用 API 等操作), 那么该怎么办呢? 这就是中间件解决的问题. 下面我们就来讲讲 redux 中的中间件.
中间件处理副作用的机制
中间件在 redux 中位于什么位置, 我们可以通过这两张图来看一下.
先来看看不用中间件时的 redux 工作流程:
dispatch 一个 action
这个 action 被 reducer 处理
reducer 根据 action 更新 store(中的 state)
而用了中间件之后的工作流程是这样的:
dispatch 一个 action
这个 action 先被中间件处理(比如在这里发送一个异步请求)
中间件处理结束后, 再发送一个 action(有可能是原来的 action, 也可能是不同的 action, 视中间件功能而不同)
中间件发出的 action 可能继续被另一个中间件处理, 进行类似 3 的步骤. 即中间件可以链式串联.
最后一个中间件处理完后, dispatch 一个符合 reducer 处理标准的 action
这个标准的 action 被 reducer 处理,
reducer 根据 action 更新 store(中的 state)
那么中间件该如何融合到 redux 中呢?
在上面的流程中, 2-4 的步骤是关于中间件的, 但凡我们想要添加一个中间件, 我们就需要写一套 2-4 的逻辑. 如果每个中间件我们手动串联的话, 就不够灵活, 增删改以及调整顺序, 都需要修改中间件串联的逻辑.
所以 redux 提供了一种解决方案, 将中间件的串联操作进行了封装, 经过封装后, 上面的步骤 2-5 就可以成为一个整体, 如下图:
我们只需要改造 store 自带的 dispatch 方法, action 发生后, 先给中间件处理, 最后再 dispatch 一个 action 交给 reducer 去改变状态.
中间件在 redux 的实现
还记得 redux 的 createStore()方法的第三个参数 enhancer 吗:
- function createStore(reducer, preloadedState, enhancer) {
- if(enhancer 是有效的){
- return enhancer(createStore)(reducer, preloadedState)
- }
- //...
- }
复制代码
在这里, 我们可以看到, enhancer(可以叫做强化器)是一个函数, 这个函数接受一个'常规 createStore 函数'作为参数, 返回一个加强后的 createStore 函数.
这个加强的过程中做的事情, 其实就是改造 dispatch, 添加上中间件. redux 提供的 applyMiddleware()方法返回的就是一个 enhancer.
applyMiddleware, 顾名思义, 应用中间件, 输入为若干中间件, 输出为 enhancer. 下面就来看看这个方法的源码:
- function applyMiddleware(...middlewares) {
- // 返回一个函数 A, 函数 A 的参数是一个 createStore 函数.
- // 函数 A 的返回值是函数 B, 其实也就是一个加强后的 createStore 函数, 大括号内的是函数 B 的函数体
- return createStore => (...args) => {
- // 用参数传进来的 createStore 创建一个 store
- const store = createStore(...args)
- // 注意, 我们在这里需要改造的只是 store 的 dispatch 方法
- let dispatch = () => { // 一个临时的 dispatch
- // 作用是在 dispatch 改造完成前调用 dispatch 只会打印错误信息
- throw new Error(` 一些错误信息 `)
- }
- // 接下来我们准备将每个中间件与我们的 state 关联起来(通过传入 getState 方法), 得到改造函数.
- const middlewareAPI = {
- getState: store.getState,
- dispatch: (...args) => dispatch(...args)
- }
- //middlewares 是一个中间件函数数组, 中间件函数的返回值是一个改造 dispatch 的函数
- // 调用数组中的每个中间件函数, 得到所有的改造函数
- const chain = middlewares.map(middleware => middleware(middlewareAPI))
- // 将这些改造函数 compose(翻译: 构成, 整理成)成一个函数
- // 用 compose 后的函数去改造 store 的 dispatch
- dispatch = compose(...chain)(store.dispatch)
- // compose 方法的作用是, 例如这样调用:
- // compose(func1,func2,func3)
- // 返回一个函数: (...args) => func1( func2( func3(...args) ) )
- // 即传入的 dispatch 被 func3 改造后得到一个新的 dispatch, 新的 dispatch 继续被 func2 改造...
- // 返回 store, 用改造后的 dispatch 方法替换 store 中的 dispatch
- return {
- ...store,
- dispatch
- }
- }
- }
复制代码
总结一下, applyMiddleware 的作用是:
从 middleware 中获取改造函数
把所有改造函数 compose 成一个改造函数
改造 dispatch 方法
总结
至此, redux 的核心源码已经讲完了, 最后不得不感叹, redux 写的真的美, 真 tm 的简洁.
redux 的核心功能还是创建一个 store 来管理 state. 通过 reducer 的层级划分, 可以得到一颗 state 树, 这棵树如何与其他框架 (如 react) 共同工作, 我会再写一篇《react-redux 源码解读》的博客探究探究这个问题, 敬请期待.
来源: https://juejin.im/post/5b9617835188255c781c9e2f