前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redux源码解读

Redux源码解读

作者头像
ayqy贾杰
发布2019-06-12 12:37:51
4570
发布2019-06-12 12:37:51
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

写在前面

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可能有一波蓄谋已久的变化

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.结构
  • 二.设计理念
  • 三.技巧
    • minified检测
      • 无干扰throw
        • master-dev queue
          • compose util
            • 充分利用自身机制
            • 四.applyMiddleware
            • 五.源码分析
            相关产品与服务
            消息队列 TDMQ
            消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档