Redux源码解读

2019-06-12 12:37:51 浏览数 (1)

写在前面

API设计很精简的库,有一些精致的小技巧和函数式的味道

一.结构

代码语言:javascript复制
src/
│  applyMiddleware.js
│  bindActionCreators.js
│  combineReducers.js
│  compose.js
│  createStore.js
│  index.js
│
└─utils/
       warning.js

index暴露出所有API:

代码语言:javascript复制
export {
 createStore,      // 关键
 combineReducers,  // reducer组合helper
 bindActionCreators,   // wrap dispatch
 applyMiddleware,  // 中间件机制
 compose           // 送的,函数组合util
}

最核心的两个东西是createStoreapplyMiddleware,地位相当于coreplugin

二.设计理念

核心思路与Flux相同:

代码语言:javascript复制
(state, action) => state

在源码(createStore/dispatch())中的体现:

代码语言:javascript复制
try {
 isDispatching = true
 // 重新计算state
 // (state, action) => state 的Flux基本思路
 currentState = currentReducer(currentState, action)
} finally {
 isDispatching = false
}

currentStateaction传入顶层reducer,经reducer树逐层计算得到新state

没有dispatcher的概念,每个action过来,都从顶层reducer开始流经整个reducer树,每个reducer只关注自己感兴趣的action制造一小块statestate树与reducer树对应,reducer计算过程结束,就得到了新的state,丢弃上一个state

P.S.关于Redux的更多设计理念(action, store, reducer的作用及如何理解),请查看Redux

三.技巧

minified检测

代码语言:javascript复制
function isCrushed() {}// min检测,在非生产环境使用min的话,警告一下
if (
 process.env.NODE_ENV !== 'production' &&
 typeof isCrushed.name === 'string' &&
 isCrushed.name !== 'isCrushed'
) {
 // warning(...)
}

代码混淆会改变isCrushedname,作为检测依据

无干扰throw

代码语言:javascript复制
// 小细节,开所有异常都断点时能追调用栈,不开不影响
// 生产环境也可以保留
try {
   throw new Error('err')
} catch(e) {}

对比velocity里用到的异步throw技巧:

代码语言:javascript复制
/!!! 技巧,异步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也是随便起的,但比较形象),姑且叫它可变队列

代码语言:javascript复制
// 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()
 }
}

写和读要做一些额外的操作:

代码语言:javascript复制
// 写
ensureCanMutateNextListeners();
updateNextListeners();// 读
currentListeners = nextListeners;

相当于写的时候新开个dev分支(没有的话),读的时候把dev merge到master并删除dev分支

用在listener队列场景非常合适:

代码语言:javascript复制
// 写(订阅/取消订阅)
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()
}

可以类比开车的情景:

代码语言:javascript复制
nextListeners是候车室,开车前带走候车室所有人,关闭候车室
车开走后有人要上车(subscribe())的话,新开一个候车室(slice())
人先进候车室,下一趟才带走,不允许空降
下车时也一样,车没停的话,先通过候车室记下谁要下车,下一趟不带他了,不允许跳车

很有意思的技巧,与git工作流神似

compose util

代码语言:javascript复制
function compose(...funcs) {
 return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

用来实现函数组合:

代码语言:javascript复制
compose(f, g, h) === (...args) => f(g(h(...args)))

核心是reduce(即reduceLeft),具体过程如下:

代码语言:javascript复制
// 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)))

注意两个顺序

代码语言:javascript复制
参数求值从内向外:f3-f2-f1 即从右向左
函数调用从外向内:f1-f2-f3 即从左向右

applyMiddleware部分有用到这种顺序,在参数求值过程bind next(从右向左),在函数调用过程next()尾触发(从左向右)。所以中间件长的比较奇怪

代码语言:javascript复制
// 中间件结构
let m = ({getState, dispatch}) => (next) => (action) => {
 // todo here
 return next(action);
};

是有原因的

充分利用自身机制

起初比较疑惑的一点是:

代码语言:javascript复制
function createStore(reducer, preloadedState, enhancer) {
 // 计算第一个state
 dispatch({ type: ActionTypes.INIT })
}

明明可以直接点,比如store.init(),为什么自己还非得走dispatch?实际上有2个作用:

  • 特殊typecombineReducer中用作reducer返回值合法性检查,作为一个简单action用例
  • 并标志着此时的state是初始的,未经reducer计算

reducer合法性检查时直接把这个初始action丢进去执行了2遍,省了一个action case,此外还省了初始环境的标识变量和额外的store.init方法

充分利用了自身的dispatch机制,相当聪明的做法

四.applyMiddleware

这一部分源码被challenge最多,看起来比较迷惑,有些难以理解

再看一下中间件的结构:

代码语言:javascript复制
// 中间件结构
//                fn1                 fn2         fn3
let m = ({getState, dispatch}) => (next) => (action) => {
 // todo here
 return next(action);
};

怎么就非得用个这么丑的高阶函数

代码语言:javascript复制
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计算过程

所以中间件结构中高阶函数每一层都有特定作用

代码语言:javascript复制
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可能有一波蓄谋已久的变化

0 人点赞