Redux 是试图让 state 的变化变得可预测,梳理复杂状态的 JavaScript 状态容器。遵循数据单向流动,严格的单向数据流是 Redux 架构的设计核心。Redux 的源码分析文章非常多,但是往往只是解释了每句代码做了什么,少有解释为什么这样做的文章。本文记录了笔者对 Redux 的核心文件 createStore.js 的分析与思考。
分析 Redux 源码 除了对 Redux 理解更加深入以外,重要的是回头使用起来更加得心应手。
Redux 本身很简单,小细节也非常适合笔者这样的前端小白研究学习。在文章的源码解析过程中笔者会穿插一些代码风格、逻辑处理方式等小菜鸡的代码思考环节。
下文对 Redux 具体概念请参阅 Redux 中文官方文档。
createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
...
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
function ensureCanMutateNextListeners() { ... }
function getState() { ... }
function subscribe(listener) { ... }
function dispatch(action) { ... }
function replaceReducer(nextReducer) { ... }
function observable() { ... }
...
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
createStore()
是整个 createStore.js 中最外层的方法,功能是创建一个 Redux store 来以存放应用中所有的 state。应用中应有且仅有一个 store。上述代码简单罗列主要内容,subscribe
、dispatch
、getState
等全部都在其内部实现。下文为了简洁说明会将代码拆分,之后分析代码全部包含在 createStore()
中,配合文件源码研究更加清晰。
export default function createStore(reducer, preloadedState, enhancer) {
// 判断 preloadedState 是一个函数,并且 enhancer 是未定义的,将 preloadedState 赋值给 enhancer 后置为 undefined
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 检查 enhancer
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 如果 enhancer 符合条件,得到一个 enhancer 后的 `createStore`
return enhancer(createStore)(reducer, preloadedState)
}
...
}
enhancer 是由 applyMiddleware 函数输出。源码注释中分割线上面的代码表示,存在中间件的情况下,改造 dispatch,融入中间件,用于处理异步处理等操作。
export default function createStore(reducer, preloadedState, enhancer) {
...
// reducer 必须是函数
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
// 获取 reducer - (state, action) => state
let currentReducer = reducer
// 拿到当前 State
let currentState = preloadedState
// 初始化 listeners 用于放置监听函数,用于保存快照供当前 dispatch 使用
let currentListeners = []
// 指向当前 listeners,在需要修改时复制出来修改为下次快照存储数据,不影响当前订阅
let nextListeners = currentListeners
// 用于标记是否正在进行 dispatch,用于控制 dispatch 依次调用不冲突
let isDispatching = false
// 写时复制,确保可以改变 nextListeners。没有新的监听可以始终用同一个引用
function ensureCanMutateNextListeners() {
// 需要写入新的监听,之前没有复制出来过的话就复制出来
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
...
}
currentListeners
是订阅器在每次 dispatch()
调用之前保存的一份快照。当你新增或取消订阅 listener 时,新的 listener 将修改内容复制到 nextListeners
里,对当前的 dispatch() 不会有任何影响。对于下一次的 dispatch(),无论嵌套与否,都会使用订阅列表里最近的一次快照。createStore.js
中仅有的两个调用 ensureCanMutateNextListeners()
位置就是在 subscribe
方法与 unsubscribe
函数块中。
ensureCanMutateNextListeners
函数笔者理解为写时复制,如果在程序的生命周期中调用了 n 次 dispatch()
就为同一个 Listeners 复制 n 次,这样的性能显然不能容忍。写时复制对性能来说是一步非常好的优化,在一段时间内始终没有新的订阅或取消订阅的情况下,nextListeners 与 currentListeners 可以共用内存。如果在下次 dispatch 之前新加了监听,此时调用此函数再进行复制,不影响修改未来订阅。
getState
export default function createStore(reducer, preloadedState, enhancer) {
...
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
...
}
内容是从 Store 中读取当前稳定的状态树 State。它与 store 的最后一个 reducer 返回值相同。
String 的换行能看出作者每行最大字符接受范围的清晰代码风格。
subscribe
export default function createStore(reducer, preloadedState, enhancer) {
...
function subscribe(listener) {
// 检测新订阅的内容是否是函数
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
// 订阅的 Bool 标记
let isSubscribed = true
// 确保拿到一份 nextListeners 不影响当前订阅的 Listeners
ensureCanMutateNextListeners()
// 将新的订阅加入订阅数组
nextListeners.push(listener)
// 返回一个取消订阅函数
return function unsubscribe() {
// 如果已取消订阅则不重复取消
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
// 取消订阅,将标志位置为 false
isSubscribed = false
// 确保拿到一份 nextListeners 不影响当前订阅的 Listeners
ensureCanMutateNextListeners()
// 找到需要取消订阅的 listener index
const index = nextListeners.indexOf(listener)
// 移除
nextListeners.splice(index, 1)
}
}
...
}
添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。
为什么不把 subscribe 和 unsubscribe 用两个独立的代码实现?
如果订阅与取消订阅是两个独立的函数,使用者难免会不小心移除不存在的监听内容,即便代码是安全的也显得混乱。将 unsubscribe 作为返回值可以按需获取,不需要移除监听的可以不取值,需要移除监听也可以一一对应,代码层级清晰明确。
dispatch
export default function createStore(reducer, preloadedState, enhancer) {
...
function dispatch(action) {
// 标准情况下 action 必须是对象,
// 也可以自定义中间件来传递异步操作
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// action 的 type 不能未定义
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// dispatch 正在进行,isDispatching 置为 true
isDispatching = true
// 旧 state 传入 action 执行 reducer,得到新 state
currentState = currentReducer(currentState, action)
} finally {
// dispatch 结束,isDispatching 置为 false
isDispatching = false
}
// 更新最新的监听对象,相当于:
// currentListeners = nextListeners
// const listeners = currentListeners
const listeners = (currentListeners = nextListeners)
// 遍历所有监听并执行
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
...
}
const listeners = (currentListeners = nextListeners)
这段赋值对应之前说到的:
currentListeners
是订阅器在每次dispatch()
调用之前保存的一份快照。当你新增或取消订阅 listener 时,新的 listener 将修改内容复制到nextListeners
里,对当前的 dispatch() 不会有任何影响。对于下一次的 dispatch(),无论嵌套与否,都会使用订阅列表里最近的一次快照。
从 dispatch()
的源码中看,此时 listeners 是这个 dispatch 的 currentListeners,在遍历期间有新的 subscribe/unsubscribe
操作都会存到当前的 nextListeners 中,不影响 listeners 的遍历。
什么时候会出现这样不安全的情况呢?
subscribe()
传入的参数 listener
就是一个函数,如果在函数里订阅或取消订阅,遍历过程中自然会出现 listeners 数组被改变的情况。currentListeners
、nextListeners
、ensureCanMutateNextListeners
就是针对这种情况设计的。
replaceReducer
export default function createStore(reducer, preloadedState, enhancer) {
...
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
...
}
replaceReducer()
的功能是替换 reducer 以及初始化 Store。官方表示:
这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。
observable
export default function createStore(reducer, preloadedState, enhancer) {
...
function observable() {
// 拿到订阅方法的函数
const outerSubscribe = subscribe
return {
// 一个最小可观察订阅方法
subscribe(observer) {
// 判断 observer 是一个对象
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
// 观察者对象应该有一个 'next' 方法
// 观察者状态改变则获取当前 state 并调用 next 方法
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
// 取消订阅的方法
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
// 返回为对象的私有属性,一般不暴露给开发者使用
[$$observable]() {
return this
}
}
}
...
}
Redux 内部没有用到这个方法,在测试代码 redux/test/createStore.spec.js 中有出现。
ECMAScript Observable 是响应式编程的一个思想与实现提议,更多信息戳 -> proposal- obserservable。
结语
看源码可以从中学到设计思路,了解优秀的框架是如何实现。除此之外,赏心悦目的 API 设计、编码顾全大局的取舍等细节也非常值得学习。细细揣摩作者的设计意图也是很有趣的。