近日偷偷摸鱼,终于良心有一丝过不去。于是 clone
了 redux
源码,打算好好研究下 redux
的核心 createStore
,没想到意外的简单,相比 vue react 源码,简直是萌萌哒的小天使。当然内部实现细节依然很精致,那么随我看看 createStore
源码真面目。
初探
先写个简单的 demo
import { createStore } from 'redux'
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return { count: state.count++, date: action.date }
case 'del':
return { add: state.count--, date: action.date }
default:
return state
}
}
const store = createStore(reducer, { count: 0, date: new Date().valueOf() })
store.dispatch({ type: 'add', date: new Date().valueOf() })
console.log(store.getState())
从上面可以看出 createStore
接收两个参数:reducer
函数以及初始 state
,通过返回的 store
调用 dispatch
修改内部状态、通过 store.getState()
获取到最新的 state
如果只是上述要求,我们可以写个简易的 redux
function createStore(reducer, preloadedState) {
let currentState = preloadedState
function dispatch(action) {
currentState = reducer(currentState, action)
}
// redux 会在初始调用一次 dispatch
dispatch(currentState)
return {
dispatch,
getState() {
return preloadedState
}
}
}
似乎也可以运行,难道 createStore
的原理就这么简单么?当然不是,上述只是针对一个小的场景。那我们来一步一步剖析一下 createStore
剩下的内容是什么?
进阶
redux
会在内部会对 reducer
做一个调用,但 reducer
显然不是随心所欲的,如下栗子
const reducer = (state, action) => {
switch (action.type) {
case 'add':
// × reducer 内部调用 dispatch ,会再次触发 reducer ,reducer 再次调用 dispatch,陷入死循环
// 类似 fn(){ fn() }
store.dispatch()
return { count: state.count++, date: action.date }
case 'del':
// × 甚至无法再调用 getState ,因为已经 reducer 函数已经将 state 通过参数传下来了
store.getState()
return { add: state.count--, date: action.date }
default:
return state
}
}
因此,在
createStore
中,我们需要阻止使用者在reducer
重复调用dispatch
看下面的代码,如果想阻止使用者重复调用,那我们就需要一个变量确定 dispatch
是否正在执行
function createStore(reducer, preloadedState) {
let currentState = preloadedState
let isDispatching = false
function dispatch(action) {
if (isDispatching) {
throw new Error('请勿在dispatch执行过程中重复调用')
}
isDispatching = true
currentState = reducer(currentState, action)
isDispatching = false
}
// redux 会在初始调用一次 dispatch
dispatch(currentState)
return {
dispatch,
getState() {
if (isDispatching) {
throw new Error('请使用 reducer 传入的 state')
}
return preloadedState
}
}
}
解决了在 reducer
中调用 dispatch
,我们来看 redux
的下一个功能 subscribe
(订阅)
demo
const store = createStore(reducer, { count: 0, date: new Date().valueOf() })
const sub1 = () => console.log('sub1')
const sub2 = () => {
console.log('sub2')
// do something
store.subscribe(() => console.log('sub3'))
unsubscribe2()
}
// 添加订阅
const unsubscribe1 = store.subscribe(sub1)
const unsubscribe2 = store.subscribe(sub2)
// dispatch 修改 state 后,触发存入的订阅函数
store.dispatch({ type: 'add', date: new Date().valueOf() }) // sub1 sub2 触发
// 卸载 sub1
unsubscribe1()
从上面的 demo 可以看到 redux
通过 store.subscribe
存入 sub
,返回一个 unsubscribe
函数,执行卸载存入的 sub
,那么源码是如何实现的呢?
function createStore(reducer, preloadedState) {
// 用来存放sub
const listeners = []
let currentState = preloadedState
let isDispatching = false
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
listeners.splice(listeners.indexOf(listener), 1)
}
}
function dispatch(action) {
if (isDispatching) {
throw new Error('请勿在dispatch执行过程中重复调用')
}
isDispatching = true
currentState = reducer(currentState, action)
// state 修改完毕,触发订阅 | for 循环只是一种迭代方式、forEach、for of 都可以
for (let index = 0; index < listeners.length; index++) {
const listener = listeners[index]
listener()
}
isDispatching = false
}
// redux 会在初始调用一次 dispatch
dispatch(currentState)
return {
subscribe,
dispatch,
getState() {
if (isDispatching) {
throw new Error('请使用 reducer 传入的 state')
}
return preloadedState
}
}
}
似乎也没什么难度,那源码做了哪些我们不知道的事?
首先,isDispatching
一视同仁,你不能在 reducer
中添加订阅 。其次是 subscribe
存入的 sub
和 reducer
不同,是可以在订阅函数中继续调用 subscribe
,但这带来一个问题
const sub1 = () => console.log('sub1')
const sub2 = () => {
console.log('sub2')
const sub3 = () => console.log('sub3')
store.subscribe(sub3)
}
store.subscribe(sub1)
store.subscribe(sub2)
// dispatch 修改 state 后,触发存入的订阅函数
store.dispatch({ type: 'add', date: new Date().valueOf() })
// 此时订阅的 sub 都被触发,会输出什么?
// 'sub1 sub2' 还是 'sub1 sub2 sub3'
会打印出 'sub1 sub2 sub3',这是一个很简单的道理:
const arr = [1, 2, 3]
for (let index = 0; index < arr.length; index++) {
console.log(arr[index])
if (arr[index] === 2) {
arr.push(4)
}
}
// 最终会输出 1 2 3 4
回到上上个 demo 中,当
dispatch
内部的 for 循环执行到sub2
,store.subscribe
又存入了sub3
,listeners.length + 1
,于是本该结束的 for 循环继续执行,触发sub3
。
但这并不符合我们想要的结果,假设你每天都会为明天要做的事列一份清单,突然接到一个通知需要你后天参加一个会议,你并没有跨天写备忘录的习惯,于是你只能写上 记得为明天添加一条备忘录:参加会议。等到明天看到这条记录时,你在明日清单写上 参加会议。
ok,映射到代码中,第一天你存入了 sub1
sub2
,第二天执行sub1
sub2
,sub2
里你为第三天存入 sub3
,因此当第二天就执行 sub3
显然是不对的。
想第二天只执行 sub1
sub2
,代码里的实现也是比较简单的
let currentArr = [1, 2, 3]
// 拷贝一个和 currentArr 相同的全新数组
const nextArr = currentArr.slice()
for (let index = 0; index < currentArr.length; index++) {
// 从 currentArr 里正常读 1 2 3
console.log(currentArr[index])
// 即使 nextArr push 也不影响 currentArr
if (currentArr[index] === 2) {
nextArr.push(4)
}
}
// 最终会输出 1 2 3
currentArr = nextArr
但是这样每次都要拷贝一份新数组,即使数组内部没变更也会做一次拷贝,对性能有些浪费,于是我们可以做一些优化:
function createStore(reducer, preloadedState) {
// currentListeners 用来 读,不做 push splice 的写入操作
let currentListeners = []
// nextListeners 用来 写入订阅
let nextListeners = currentListeners
let currentState = preloadedState
let isDispatching = false
function ensureCanMutateNextListeners() {
// 这里是不是会有些疑惑,为什么相等了还要拷贝 往下看
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
// 如果你看完 subscribe 以及 dispatch 函数后请读这段话
// 因此我们可以看到,每次调用 store.subscribe()都会执行 ensureCanMutateNextListeners 判断 nextListeners 与 currentListeners
// 默认情况下 let nextListeners = currentListeners ,nextListeners 与 currentListeners 相等
// 拷贝出一个新的 nextListeners ,因为你调用 ensureCanMutateNextListeners 函数,就代表你将要 push 或 splice 一个 listener
// 增加或者删除一个 listener 后,等待 dispath ,currentListeners = nextListeners,
// 开始遍历 currentListeners 中的 listener,
// 不管是执行 listener 的过程中重复掉起 store.subscribe(),
// 还是你在 n遍 dispatch 后调用 store.subscribe()
// 只要你掉 store.subscribe() 就代表你要往 nextListeners 做写操作,那我就给你一个新的 nextListeners
// 反正你下次 dispatch 读的时候 我都会 currentListeners = nextListeners
}
function subscribe(listener) {
// 不允许在 reducer 调用
if (isDispatching) {
throw new Error('请勿在dispatch执行过程中重复调用')
}
// currentListeners 和 nextListeners 相同,获得一个拷贝后的 nextListeners
ensureCanMutateNextListeners()
nextListeners.push(listener)
// 每次调用 store.subscribe() 保存一个 isSubscribed 变量
let isSubscribed = true
return function unsubscribe() {
// unsubscribe 作为卸载订阅的作用只能调用一次
// isSubscribed 为 true,正常执行,但是在 unsubscribe 函数末尾会设置为 false
// 因此再调用 unsubscribe --> !false => return
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error('请勿在dispatch执行过程中重复调用')
}
ensureCanMutateNextListeners()
nextListeners.splice(nextListeners.indexOf(listener), 1)
isSubscribed = false
}
}
function dispatch(action) {
if (isDispatching) {
throw new Error('请勿在dispatch执行过程中重复调用')
}
isDispatching = true
currentState = reducer(currentState, action)
// 现在开始读取执行 listener
// 将 nextListeners 赋值给 currentListeners,正常读取
const listeners = (currentListeners = nextListeners)
for (let index = 0; index < listeners.length; index++) {
const listener = listeners[index]
listener()
// 遇到特殊情况 sub 中含有 store.subscribe(dep3) 回到 subscribe 函数
// 此时 const listeners = (currentListeners = nextListeners) 后,二者相等
// 执行 ensureCanMutateNextListeners 函数 得到一个新的 nextListeners
// nextListeners push 一个 listener,但新的 nextListeners 并不影响 currentListeners
// nextListeners 为 [oldListener, newListener], currentListeners 依旧为 [oldListener]
// 本次循环结束
// 等待下次 dispatch , const listeners = (currentListeners = nextListeners)
// currentListeners 得到了 nextListeners 中的 [oldListener, newListener]
}
isDispatching = false
}
dispatch(currentState)
return {
subscribe,
dispatch,
getState() {
if (isDispatching) {
throw new Error('请使用 reducer 传入的 state')
}
return preloadedState
}
}
}
这种读写分离的方式也是我查阅资料的时候看到读写分离的双缓冲技术。(PS:这是什么高大上的鬼名字)
上面的 demo 中,即使是
subscribe
中也会通过isDispatching
禁止在reducer
调用,这是因为redux
约定reducer
需为一个纯函数,所以尽量从redux
内部规避一些行为
其他
到这里我们的 createStore
源码就结束了,剩余的 replaceReducer
比较简单、observable
和 rxjs
相关,但我对 rxjs
没什么了解。
思考
但我们可以通过这次源码得到一些思考,那就是什么是状态管理器? 前段时间我逛论坛的时候,看到一句话:
最好的状态管理器就是 {} [狗头]
回到我们最初的代码
不置可否。我们对状态管理的初衷是什么?是希望脱离组件外,有一个数据,可以在全部或者几个组件内使用,不需要定义繁杂的 props 及 onHandle。redux
本身只提供了一层桥接,你的数据(state)与行为(action)的桥接(reducer)。它为你的共享数据做了限制与规范,你不能随心所欲的 state.count++
,必须规定一个行为 action.type === 'add'
,这或许就是 redux
官网的 “设计思想”。这一点和 react
路线相反 vue
状态管理 vuex
竟有些相似,mutation
就是另一种格式的 reducer
。这些状态管理实现方式或许有差别,但背后的设计思想却出奇的一致。
可以参考这个 回答。
视图或者组件以单向数据流的形式得到最新值,为每次数据更新添加一个行为,数据发生改变,更好的知晓哪里做的变更。
当然这带来另一个问题,业务变庞大, reducer
mutation
的编写过程就会十分痛苦。