写在前面
API设计很精简的库,有一些精致的小技巧和函数式的味道
一.结构
src/
│ applyMiddleware.js
│ bindActionCreators.js
│ combineReducers.js
│ compose.js
│ createStore.js
│ index.js
│
└─utils/
warning.js
index
暴露出所有API:
export {
createStore, // 关键
combineReducers, // reducer组合helper
bindActionCreators, // wrap dispatch
applyMiddleware, // 中间件机制
compose // 送的,函数组合util
}
最核心的两个东西是createStore
和applyMiddleware
,地位相当于core
和plugin
二.设计理念
核心思路与Flux相同:
(state, action) => state
在源码(createStore/dispatch()
)中的体现:
try {
isDispatching = true
// 重新计算state
// (state, action) => state 的Flux基本思路
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
把currentState
和action
传入顶层reducer
,经reducer
树逐层计算得到新state
没有dispatcher
的概念,每个action
过来,都从顶层reducer
开始流经整个reducer
树,每个reducer
只关注自己感兴趣的action
,制造一小块state
,state
树与reducer
树对应,reducer
计算过程结束,就得到了新的state
,丢弃上一个state
P.S.关于Redux的更多设计理念(action, store, reducer
的作用及如何理解),请查看Redux
三.技巧
minified检测
function isCrushed() {}
// min检测,在非生产环境使用min的话,警告一下
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
// warning(...)
}
代码混淆会改变isCrushed
的name
,作为检测依据
无干扰throw
// 小细节,开所有异常都断点时能追调用栈,不开不影响
// 生产环境也可以保留
try {
throw new Error('err')
} catch(e) {}
对比velocity里用到的异步throw技巧:
/!!! 技巧,异步throw,不会影响逻辑流程
setTimeout(function() {
throw error;
}, 1);
同样都不影响逻辑流程,无干扰throw
的好处是不会丢失调用栈之类的上下文信息,具体如下:
This error was thrown as a convenience so that if you enable “break on all exceptions” in your console, it would pause the execution at this line.
master-dev queue
这个技巧没有很合适的名字(master-dev queue也是随便起的,但比较形象),姑且叫它可变队列:
// 2个队列,current不能直接修改,要从next同步,就像master和dev的关系
// 用来保证listener执行过程不受干扰
// 如果subscribe()时listener队列正在执行的话,新注册的listener下一次才生效
let currentListeners = []
let nextListeners = currentListeners
// 把nextListeners作为备份,每次只修改next数组
// flush listener queue之前同步
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
写和读要做一些额外的操作:
// 写
ensureCanMutateNextListeners();
updateNextListeners();
// 读
currentListeners = nextListeners;
相当于写的时候新开个dev
分支(没有的话),读的时候把dev
merge到master
并删除dev
分支
用在listener
队列场景非常合适:
// 写(订阅/取消订阅)
function subscribe(listener) {
// 不允许空降
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// 不允许跳车
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 读(flush queue执行所有listener)
// 同步两个listener数组
// flush listener queue过程不受subscribe/unsubscribe干扰
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
可以类比开车的情景:
nextListeners是候车室,开车前带走候车室所有人,关闭候车室
车开走后有人要上车(subscribe())的话,新开一个候车室(slice())
人先进候车室,下一趟才带走,不允许空降
下车时也一样,车没停的话,先通过候车室记下谁要下车,下一趟不带他了,不允许跳车
很有意思的技巧,与git工作流神似
compose util
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
用来实现函数组合:
compose(f, g, h) === (...args) => f(g(h(...args)))
核心是reduce
(即reduceLeft),具体过程如下:
// Array reduce API
arr.reduce(callback(accumulator, currentValue, currentIndex, array)[, initialValue])
// 输入 -> 输出
[f1, f2, f3] -> f1(f2(f3(...args)))
1.做((a, b) => (...args) => a(b(...args)))(f1, f2)
得到accumulator = (...args) => f1(f2(...args))
2.做((a, b) => (...args) => a(b(...args)))(accumulator, f3)
得到accumulator = (...args) => ((...args) => f1(f2(...args)))(f3(...args))
得到accumulator = (...args) => f1(f2(f3(...args)))
注意两个顺序:
参数求值从内向外:f3-f2-f1 即从右向左
函数调用从外向内:f1-f2-f3 即从左向右
applyMiddleware
部分有用到这种顺序,在参数求值过程bind next
(从右向左),在函数调用过程next()
尾触发(从左向右)。所以中间件长的比较奇怪:
// 中间件结构
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
是有原因的
充分利用自身机制
起初比较疑惑的一点是:
function createStore(reducer, preloadedState, enhancer) {
// 计算第一个state
dispatch({ type: ActionTypes.INIT })
}
明明可以直接点,比如store.init()
,为什么自己还非得走dispatch
?实际上有2个作用:
特殊
type
在combineReducer
中用作reducer
返回值合法性检查,作为一个简单action
用例并标志着此时的
state
是初始的,未经reducer
计算
reducer
合法性检查时直接把这个初始action
丢进去执行了2遍,省了一个action case
,此外还省了初始环境的标识变量和额外的store.init
方法
充分利用了自身的dispatch
机制,相当聪明的做法
四.applyMiddleware
这一部分源码被challenge最多,看起来比较迷惑,有些难以理解
再看一下中间件的结构:
// 中间件结构
// fn1 fn2 fn3
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
怎么就非得用个这么丑的高阶函数?
function applyMiddleware(...middlewares) {
// 给每一个middleware都注入{getState, dispatch} 剥掉fn1
chain = middlewares.map(middleware => middleware(middlewareAPI))
// fn = compose(...chain)是reduceLeft从左向右链式组合起来
// fn(store.dispatch)把原始dispatch传进去,作为最后一个next
// 参数求值过程从右向左注入next 剥掉fn2,得到一系列(action) => {}的标准dispatch组合
// 调用被篡改过的disoatch时,从左向右传递action
// action先按next链顺序流经所有middleware,最后一环是原始dispatch,进入reducer计算过程
dispatch = compose(...chain)(store.dispatch)
}
重点关注fn2
是怎样被剥掉的:
// 参数求值过程从右向左注入next 剥掉fn2 dispatch = compose(…chain)(store.dispatch)
如注释:
fn = compose(...chain)
是reduceLeft从左向右链式组合起来fn(store.dispatch)
把原始dispatch
传进去,作为最后一个next
(最内层参数)上一步参数求值过程从右向左注入next 剥掉fn2
利用reduceLeft参数求值过程bind next
再看调用过程:
调用被篡改过的
disoatch
时,从左向右传递action
action
先按next
链顺序流经所有middleware
,最后一环是原始dispatch
,进入reducer
计算过程
所以中间件结构中高阶函数每一层都有特定作用:
fn1 接受middlewareAPI注入
fn2 接受next bind
fn3 实现dispatch API(接收action)
applyMiddleware
将被重构,更清楚的版本见pull request#2146,核心逻辑就是这样,重构可能会考虑要不要做break change,是否支持边界case,够不够易读(很多人关注这几行代码,相关issue/pr至少有几十个)等等,Redux维护团队比较谨慎,这块的迷惑性被质疑了非常多次才决定要重构
五.源码分析
Git地址:https://github.com/ayqy/redux-3.7.0
P.S.注释足够详尽。虽然最新的是3.7.2
了,但不会有太大差异,4.0
可能有一波蓄谋已久的变化