redux 源码阅读
首先从 redux 的官方示例来看 redux 的作用
import { createStore } from 'redux'
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
let store = createStore(counter)
store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
这样简单一看的话, redux 感觉上像是一个注册订阅+数据管理, 它在 store 中存有一个 state 树, 仅可通过 reducer 进行修改, 而 reducer 的第二个参数是 action, 用户通过 dispatch 特定 action 来调用 reducer 使 state 发生改变.
从官方示例中可以看出 createStore 的重要性, 所以我们的解读也从 createStore 开始
1. CreateStore
暂时去掉 enhancer (相当于一个扩展功能) 的相关代码, 便于理解 store 的运行
import isPlainObject from 'lodash/isPlainObject'
import $observable from 'symbol-observable'
export const ActionTypes = {
INIT: '@@redux/INIT'
}
export default function createStore(reducer, preloadedState) {
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
// reducer, 相当于一个派发中心, 根据传入的 action.type 来执行相应操作修改 state
let currentReducer = reducer
// state, 数据都存在这棵对象树上
let currentState = preloadedState
// 当前的订阅者队列
let currentListeners = []
// 当前订阅者队列的副本, 虽然起名是 next 但是我更愿意理解成副本/快照
let nextListeners = currentListeners
// 是否正在分发中
let isDispatching = false
// 此函数在且仅在订阅时与解除订阅时被调用
// 用处是保证当前订阅者队列与副本的订阅者队列不指向同一内存地址
// 下述有解释为什么要使用队列和他的副本以及要更新副本的内存地址
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 给外界调用, 让外界也能访问闭包里的参数
// 但是我有点奇怪, 为什么不派发一个副本? 这样外界可以不通过 action 直接修改 state
function getState() {
return currentState
}
// 传统的订阅函数, 传入 listener 并压入下一次需要调用的订阅者队列中
// 返回一个解除订阅的函数
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 简单的分发函数, 一个个调用储存在队列中的订阅者
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
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 {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
// 更换 reducer
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
// 一个观察者的封装, 当每次 state 发生变化时, 将会调用传入的 observer 的 next 方法
// 从输出的 prop `[$observable]` 来看, 应该是一个内部的方法,
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer) {
if (typeof observer !== 'object') {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$observable]() {
return this
}
}
}
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$observable]: observable
}
}
代码中有几个要点:
1.1 为什么要存在 currentListeners 跟 NextListeners?
如果不存在 NextListener 的话, 在通知订阅者(subscribe)的途中发生了其他订阅或退订, 将会导致错误和不确定性
const store = createStore(reducers.todos)
const unsubscribe1 = store.subscribe(() => {
// 这个 listener 会导致监听者队列发生改变
const unsubscribe2 = store.subscribe(()=>{})
unsubscribe2()
})
如上述代码, 当 dispatch 导致 store 改变而遍历调用订阅者(listeners)时, 因为其中有一个 listener 使监听者队列发生了改变, 如果没有 NextListeners 副本, 那么将会直接修改到 currentListeners 这个正在遍历中的数组上, 这样会导致遍历中的 index 不可信
这可能不是唯一做法? 我在查阅 redux 的 issue 时发现一个老哥很久之前就提交了一个去掉nextListeners 的做法: https://github.com/reduxjs/redux/pull/2376, 但是最后似乎因为不可预测而没有被接纳
因此需要加入 NextListeners, 不管本次通知订阅的过程中有没有发生退订, 都只影响下一次的订阅, 不会对当前结果造成影响
参考博客: Redux:自问自答, 但是举例是我自己写的, 我觉得更加好理解(x)
2. applyMiddleware
上面没有说到的 enhancer, 就是在这里体现了, applyMiddleware 将会返回一个可传入 createStore 的 enhancer
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
// 传给每个中间件的参数
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 储存每个中间件函数的返回值
chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose 是一个很广泛的编程函数概念, 实现的把第一个参数(函数)的返回值传给第二个
// 用 c 代表 chain 这里要实现的是 c3(c2(c1(dispatch)))
// redux 的 compose 是用 reduce 实现的
dispatch = compose(...chain)(store.dispatch)
// 用经过中间件修饰的 dispatch 替代原有的
return {
...store,
dispatch
}
}
}
这个applyMiddleware
其实是一个三级柯里化(简单的理解就是, 当收集完三次参数才会执行)函数, 第一次是中间件, 第二次是 createStore, 第三次是 createStore 的参数
compose
组合函数, 用的地方挺多, 体现了函数式编程的思想, 这个可以额外的看看, 就不赘述了
3. bindActionCreators
这个模块的源码其实只需要看以下函数即可, 其余的部分只是根据传入的 actionCreators 是一个对象还是一个函数来分别调用此函数而已
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
其中 actionCreator 将会返回一个 action: { type: 'xxx' }, 这个可以用于生成一系列需要同样处理的 action
例如把输入的参数...args 都作为一个 payload 生成相应的 action
-
combineReducers
这个的作用就像名字一样, 可以组合 reducers, 在大型项目中可以更加方便各模块管理自身的 reducer.
这个的源码比较长, 我们先删除一些不太重要的报错, 查看他的实现
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 先过滤掉 reducers 里不是 function 的 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)
return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}
// 遍历 reducers 对象, 查找每一个 reducer 对应的 state
// 如果传入 action 会导致这个 state 发生变化, 则记录下这次 change
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
小结
这次阅读 redux 源码让我感到自己对于闭包的理解还不够深刻, 那些柯里化及返回来返回去的函数把我都搞晕了= =, 实际项目中我应该不会使用这样的写法(因为可读性实在不太高), 但是这也增强了我阅读其他源码的水平