熟悉React的前端同学应该对Redux不陌生,它是一个成熟且小巧的状态管理工具,官方定义是
A Predictable State Container for JS Apps
。随着 JavaScript 应用日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态),state 在什么时候,由于什么原因,如何变化已然不受控制,Redux 正是希望解决这一问题,让 state 的变化变得可预测。
Redux 源码本身并不复杂,是著名的“小而美”源码,很多人推荐去读一读,之前用过Redux,出于好奇我也去拜读了一下代码,希望能了解它是怎么工作的。
1. Redux 三大原则
虽然Redux的代码非常简短,仍需要带着一些基本认识去读。Redux 是通过限制更新发生的时间和方式来让状态变化变得可预测,而限制条件反映在 Redux 的三大原则中,我们先复习下这些原则:
- 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写纯函数reducers。
那么store和state分别是什么样的数据结构,在修改state又经过了哪些过程,Redux是怎么保证这些限制得到落实?带着这些问题,我们开始读源码。
2. createStore
首先,我们先探究下store是如何被构造出来的。举一个简单的例子,我们写一个reducer后,就可以调用createStore构造一个store。
代码语言:txt复制import { createStore } from 'redux';
function anyReducer(){...}
let store = createStore(anyReducer);
console.log(store);
把它打印出来,得到的是一个带这几个API的普通对象,这就是store的全部:
从Redux工程目录的src/index.js开始,找到导出的createStore方法(基本上src也就是这几个文件 一个utils文件)
为了看清整个createStore函数的全貌,我对这部分源码进行了省略处理如下:
这个函数接受三个参数,分别代表了用户定义的如何去更新state的方法(reducer)、预赋值的state(preloadState)及enhancer(涉及中间件后面再一起了解),这些变量被以闭包的形式存储成了函数的内部变量,然后把自己的内部函数打包暴露出去,打包的结果就是我们外部得到的store对象。这里可以管中窥豹看到一些Redux的思想,大部分的代码其实是用户自己来提供的,Redux只是提供一个限制框架,用闭包的方式对外暴露有限的方法达到数据规范更新的目的。
三大原则里的单一数据源,就是通过一个currentState变量来实现的,且这里没有暴露任何直接修改state的方法,只有getState,想要修改只能通过dispatch,按照指定的规范去走流程,从而达到redux的可控可预测的目的。
2.1 dispatch & getState
在create的过程中,会主动调一次dispatch,dispatch方法的实现如下图所示,非常直接地,dispatch里核心就是调用currentRuducer返回新的state。除此之外,就是为了对action有严格限制,必须是一个简单对象plainObject、必须要有type属性,这些都能保证reducer函数处理的时候拿到的action是预期的,可以放心的去执行纯函数。
还有个小细节,reducer执行时都会用isdispatching这个flag进行标记,限制执行其他的函数,比如dispatch本身,在isdispatching为true时会抛出错误。看到这里我有疑问:
- 为什么需要这个变量?
js是单线程语言,这些函数都是同步的,既然是同步场景,我们在调用dispatch时,js会执行完这个函数再处理其他函数,应该不会有交集。后面得到结论,这个flag是标记当前正在执行reducer,reducer是用户写的,这个flag是为了不让用户在reducer方法中执行其他可能会破环正常数据流程的方法,比如在reducer中再次dispatch,会导致死循环,这也时Redux为了保护而进行限制的一种体现。在getState方法中,如下图所示,如果isdispatching是true,说明是在reducer中执行了getState,而reducer的入参里已经能直接拿到state,这时调用getState就会抛出错误:
getState方法非常简短,除了抛出错误,就是直接返回currentState。这里返回的是currentState本体,没有做拷贝,所以其实如果state是引用类型的话,是可以直接通过getState来直接修改state内部的属性值的,但是肯定不推荐这样做,不走dispatch action的话变更不会通知订阅者。
2.2 订阅和取消:Subscribe & unsubscribe
订阅和取消也是Redux store中提供的重要API,展开后的subscribe方法如下:
除去一些检查,Redux的订阅就是简单的实现了一个观察者模式,把监听的函数放进listener数组里,返回取消订阅函数,在那里面再执行slice把对应项剔除。在dispatch方法中,执行reducer更新state后,后半段把监听的函数依次执行:
这里我们注意到Redux使用了两个listener变量(nextListener和currentListener)来保存监听函数,并且在订阅和取消订阅的时候使用了ensureCanMutateListeners方法来执行浅拷贝:
这里我产生了很大的疑问,为什么要用如此不直观的方法来保存监听者。搜索了一些其他人的观点,普遍认为如果只有一个变量的话,在调用监听者的for循环过程中进行了subscribe或者unsubscribe,循环中的listener数组长度会改变,而从漏掉执行一些函数。但如果只是为了达到这个目的,在遍历前浅拷贝一次就可以了,仍然不需要去维护两个内部变量。
仔细去理解Redux的方案思路发现,nextlistener指向实时最新的数组,currentListener则更像是一份循环时的快照,当需要循环之前,因为currentListener还是上一次循环的快照,因此需要const listeners = (currentListeners = nextListeners)
把快照指向最新的数组来对齐两个变量。在需要修改订阅者时,如果二者指向同一个数组,即nextListeners === currentListeners
时,则基于快照(currentListeners)做浅拷贝生成新的nextListeners;如果不指向同一个数组,说明已经被浅拷贝过了,那么就不用再次拷贝了,直接修改nextListener即可。这个机制可以使得浅拷贝的次数最小。
2.3 动态注入reducer:replaceReducer
在一些按需加载的场景,在初始化store的时候可能会用一个基本的reducer/state,到更深层的页面的时候,可能需要切换新的reducer,也就是动态注入。store提供了replaceRducer函数,保证reducer和state可以方便切换。
替换reducer,简单粗暴,额外执行一次replace的action,类似于init:
3. combineReducers
除了基本的createStore,Redux还提供了其他API如 combineReducer。随着应用变得复杂,需要对reducer进行拆分,拆分后每个模块负责state的一部分。combineReducers函数其实就实现一个功能:将多个不同的小的reducer组合起来,得到一个最终的reducer,然后就可以对这个reducer进行createStore,得到的store的state也是各个部分组合起来的store。
这部分代码稍微有些长(相对于其他的函数来说),但基本流程很简单:
- 树形浅拷贝finalReducers
- 校验finalReducers
- 返回组合后的reducer combination:
- 依次调用reducers
- 组合states
通过这个方法返回的组合后的“reducer”(combination)并不是一个常规的reducer,它并没有处理action,只是会依次把每个子reducer都跑一遍,看有没有变更,有变更时就会把新的state组合返回。
4. bindActionCreators
在使用react-redux写mapDispatchToProps经常会使用bindActionCreators这个API,如下图
这个API可以在子组件dispatch的时候感受不到Redux的存在。它的源码如下:
就上面的bindActionCreator而言,它接受单个actionCreator和dispatch两个参数,组装成新的函数返回,新的函数会一次性完成创建(即create)、使用(即dispatch)action的过程。对外导出的bindActionCreators API可以接受多个actionCreators(即集合),然后以key-value的形式调用bindActionCreator并保存结果返回。
5. redux中间件:applyMiddleware和compose
很多框架如koa等都有中间件概念,在这些框架中,中间件可以让你在接收请求和生成响应之间放置的一些代码,在Redux中也一样,它的中间件机制在 dispatch action 和到达 reducer 的那一刻之间提供了逻辑插入点:
多层middleWare就是对dispatch的一层层包装,调用时中间件可以组合成一个链,对用户而言调用的时候是无感的,看起来仍然像调用普通的dispatch一样。
Redux本身并不包括中间件代码,只是支持应用按照规范写的中间件,或使用现成中间件(如redux-thunk)。应用中间件的API为applyMiddleware。
对于一个简单的中间件如打印简单日志,它基本长这样:
我原本对Redux中间件并不熟悉,所以先去看了一下官方概念,对我了解中间件为什么要这么写有很大帮助。中间件的3个参数做了柯里化,以便于分层次去使用它。三个参数分别为storeAPI、nextDispatch、action,参数的形式是固定的,中间件只关心函数块中做什么,以及何时去调用nextDispatch(也即下一个中间件)。最终,调用顺序如下图所示:
5.1 compose方法
在applyMiddleware之前,需要先了解Redux中的compose方法。
简单理解compose,就是compose(A, B, C)(args)会被转为A(B(C(arg)))的形式,函数顺序形式会被组合成嵌套结构,这对组装中间件非常有帮助,因为在开发者写多个中间件往往是数组的顺序形式,而执行时,下一个中间件是上一个中间件的参数。
5.2 applyMiddleware
applyMiddleware方法如下图所示:
同样,它的柯里化的参数链也非常长,语意化地去理解这些参数:它接受多个中间件作为参数,返回一个函数,该函数被称为Enhancer(增强器),增强器可以对createStore方法进行增强,也即,接受createStore方法,返回一个被增强的createStore方法,当外部调用这个增强后的createStore时,得到的就是带有中间件的store和dispatch方法。
applyMiddleware的函数内容为:
- 调用参数传入的createStore方法,创建store;
- 封装一个middlewareAPI作为store传参给middleware(该API并非真正的store,但封装getState和dispatch方法,对于middleware来说是等同的),并使用compose改变中间件之间的调用结构为嵌套;
- 得到新的dispatch,替换第一步中创建store的原始dispatch,封装成新的store返回给外部
5.3 使用applyMiddleware
使用中间件的用法为:
代码语言:txt复制const store = createStore(reducer, preloadState, applyMiddleware(m1, m2, m3...))
applyMiddleware返回的enhancer就是createStore方法的第三个参数。之前在createStore方法中涉及中间件代码先省略了,现在将其补齐:
当enhancer存在时,也即调用了applyMiddleware(m1, m2, m3...)做为第三个参数时,调用enhancer得到新的createStore方法并执行,得到带有中间件的store和dispatch方法,向外返回store。
6. 总结
以上就是Redux大部分的源码内容和我的解读,本次阅读是出于兴趣而非为了解决开发问题,力求看懂代码细节,抱着学习心态希望能在阅读代码中理解作者意图,叙述起来可能会有平铺直叙的感觉。整体看,Redux确实使用了很少的代码解决了它想解决的问题,代码设计也很巧妙,值得学习。此外还有一些关于Rxjs、RTK相关的内容因为没涉及所以本文没有讲,有兴趣的读者可以一起读一读。希望本文对希望了解Redux原理的同学有所帮助。