探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)
Redux 在几天前(2018.04.18)发布了新版本,6 commits 被合入 master。从诞生起,到如今 4.0 版本,Redux 保持了使用层面的平滑过渡。同时前不久, React 也从 15 升级到 16 版本,开发者并不需要作出太大的变动,即可“无痛升级”。但是在版本迭代的背后很多有趣的设计值得了解。Redux 此次升级同样如此。
本文将从此次版本升级展开,从源代码改动入手,进行分析。通过后文内容,相信读者能够在 JavaScript 基础层面有更深认识。
本文支持前端初学者学习,同时更适合有 Redux 源码阅读经验者,核心源码并不会重复分析,更多将聚焦在升级改动上。
改动点总览
这次升级改动点一共有 22 处,最主要体现在 TypeScript 使用、CommonJS 和 ES 构建、关于 state 抛错三方面上。对于工程和配置的改动,我们不再多费笔墨。主要从代码细节入手,基础入手,着重分析以下几处改动:
- 中间件 API dispatch 参数处理;
- applyMiddleware 改动;
- bindActionCreators 对 this 透明化处理;
- dispatching 时,对 state 的冻结;
- Plain Object 类型判断;
话不多说,我们直接进入正题。
applyMiddleware 参数处理
这项改动由 Asvarox 提出。熟悉 Redux 源码中 applyMiddleware.js 设计的读者一定对 middlewareAPI 并不陌生:对于每个中间件,都可以感知部分 store,即 middlewareAPI。这里简单展开一下:
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch)
创建一个中间件 store:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
我们看,applyMiddleware 是个三级 curry 化的函数。它将陆续获得了三个参数,第一个是 middlewares 数组,[mid1, mid2, mid3, ...],第二个是 Redux 原生的 createStore,最后一个是 reducer;
applyMiddleware 利用 createStore 和 reducer 创建了一个 store,然后 store 的 getState 方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量。middlewares 数组通过 map 方法让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍。执行完后,获得 chain 数组,[f1, f2, ... , fx, ...,fn],接着 compose 将 chain 中的所有匿名函数,[f1, f2, ... , fx, ..., fn],组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn] 将会从右到左依次执行。以上解释改动自:pure render 专栏。
好了,把中间件机制简要解释之后,我们看看这次改动。故事源于 Asvarox 设计了一个自定义的中间件,这个中间件接收的 dispatch 需要两个参数。他的“杰作”就像这样:
const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));
对比传统编写中间件的套路:
const middleware = store => next => action => {...}
我们能清晰地看到他的这种编写方式会有什么问题:在原有 Redux 源码基础上,actionCreator 参数后面的 args 将会丢失。因此他提出的改动点在:
const middlewareAPI = {
getState: store.getState,
- dispatch: (action) => dispatch(action)
+ dispatch: (...args) => dispatch(...args)
}
如果你好奇他为什么会这样设计自己的中间件,可以参考 #2501 号 issue。我个人认为对于需求来说,他的这种“奇葩”方式,可以通过其他手段来规避;但是对于 Redux 库来说,将 middlewareAPI.dispatch 参数展开,确实是更合适的做法。
此项改动我们点到为止,不再钻牛角尖。应该学到:基于 ES6 的不定参数与展开运算符的妙用。虽然一直在说,一直在提,但在真正开发程序时,我们仍然要时刻注意,并养成良好习惯。
基于此,同样的改动也体现在:
export default function applyMiddleware(...middlewares) {
- return (createStore) => (reducer, preloadedState, enhancer) => {
- const store = createStore(reducer, preloadedState, enhancer)
+ return (createStore) => (...args) => {
+ const store = createStore(...args)
let dispatch = store.dispatch
let chain = []
这项改动由 jimbolla 提出。
bindActionCreators 对 this 透明化处理
Redux 中的 bindActionCreators,达到 dispatch 将 action 包裹起来的目的。这样通过 bindActionCreators 创建的方法,可以直接调用 dispatch(action) (隐式调用)。可能很多开发者并不常用,所以这里稍微展开,在 action.js 文件中, 我们定义了两个 action creators:
function action1(){
return {
type:'type1'
}
}
function action2(){
return {
type:'type2'
}
}
在另一文件 SomeComponent.js 中,我们便可以直接使用:
import { bindActionCreators } from 'redux';
import * as oldActionCreator from './action.js'
class C1 extends Component {
constructor(props) {
super(props);
const {dispatch} = props;
this.boundActionCreators = bindActionCreators(oldActionCreator, dispatch);
}
componentDidMount() {
// 由 react-redux 注入的 dispatch:
let { dispatch } = this.props;
let action = TodoActionCreators.addTodo('Use Redux');
dispatch(action);
}
render() {
// ...
let { dispatch } = this.props;
let newAction = bindActionCreators(oldActionCreator, dispatch)
return <Child {...newAction}></child>
}
}
这样一来,我们在子组件 Child 中,直接调用 newAction.action1 就相当于调用 dispatch(action1),如此做的好处在于:没有 store 和 dispatch 的组件,也可以进行动作分发。
一般这个 API 应用不多,至少笔者不太常用。因此上面做一个简单介绍。有经验的开发中一定不难猜出 bindActionCreators 源码做了什么,连带着这次改动:
function bindActionCreator(actionCreator, dispatch) {
- return (...args) => dispatch(actionCreator(...args))
+ return function() { return dispatch(actionCreator.apply(this, arguments)) }
}
我们看这次改动,对 actionCreator 使用 apply 方法,明确地进行 this 绑定。那么这样做的意义在哪里呢?
我举一个例子,想象我们对原始的 actionCreator 进行 this 绑定,并使用 bindActionCreators 方法:
const uniqueThis = {};
function actionCreator() {
return { type: 'UNKNOWN_ACTION', this: this, args: [...arguments] }
};
const action = actionCreator.apply(uniqueThis,argArray);
const boundActionCreator = bindActionCreators(actionCreator, store.dispatch);
const boundAction = boundActionCreator.apply(uniqueThis,argArray);
我们应该期望 boundAction 和 action 一致;且 boundAction.this 和 uniqueThis 一致,都等同于 action.this。这如此的期望下,这样的改动无疑是必须的。
对 state 的冻结
Dan Abramov 认为,在 reducer 中使用 getState() 和 subscribe() 方法是一种反模式。store.getState 的调用会使得 reducer 不纯。事实上,原版已经在 reducer 执行过程中,禁用了 dispatch 方法。源码如下:
function dispatch(action) {
// ...
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
同时,这次修改在 getState 方法以及 subscribe、unsubscribe 方法中进行了同样的冻结处理:
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.'
)
}
笔者认为,这样的做法毫无争议。显式抛出异常无意是合理的。
Plain Object 类型判断
Plain Object 是一个非常有趣的概念。这次改动围绕判断 Plain Object 的性能进行了激烈的讨论。最终将引用 lodash isPlainObject 的判断方法改为 ./utils/isPlainObject 中自己封装的做法:
- import isPlainObject from 'lodash/isPlainObject';
+ import isPlainObject from './utils/isPlainObject'
简单来说,Plain Object:
指的是通过字面量形式或者new Object()形式定义的对象。
Redux 这次使用了以下代码来进行判断:
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
如果读者对上述代码不理解,那么需要补一下原型、原型链的知识。简单来说,就是判断 obj 的原型链有几层,只有一层就返回 true。如果还不理解,可以参考下面示例代码:
function Foo() {}
// obj 不是一个 plain object
var obj = new Foo();
console.log(typeof obj, obj !== null);
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// false
var isPlain = Object.getPrototypeOf(obj) === proto;
console.log(isPlain);
而 loadash 的实现为:
function isPlainObject(value) {
if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}
export default isPlainObject
isObjectLike 源码:
function isObjectLike(value) {
return typeof value == 'object' && value !== null
}
baseGetTag 源码:
const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
function baseGetTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
if (!(symToStringTag && symToStringTag in Object(value))) {
return toString.call(value)
}
const isOwn = hasOwnProperty.call(value, symToStringTag)
const tag = value[symToStringTag]
let unmasked = false
try {
value[symToStringTag] = undefined
unmasked = true
} catch (e) {}
const result = toString.call(value)
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag
} else {
delete value[symToStringTag]
}
}
return result
}
根据 timdorr 给出的对比结果,dispatch 方法中:
master: 4690.358ms
nodash: 82.821ms
这一组 benchmark 引发的讨论自然少不了,也引出来了 Dan Abramov。笔者对此不发表任何意见,感兴趣的同学可自行研究。从结果上来看,摒除了部分对 lodash 的依赖,在性能表现上说服力增强。
展望和总结
提到 Redux 发展,自然离不开 React,React 新版本一经推出,极受追捧。尤其是 context 这样的新 API,某些开发者认为将逐渐取代 Redux。
笔者认为,围绕 React 开发应用,数据状态管理始终是一个极其重要的话题。但是 React context 和 Redux 并不是完全对立的。
首先 React 新特性 context 在大型数据应用的前提下,并不会减少模版代码。而其 Provider 和 Consumer 的一一对应特性,即 Provider 和 Consumer 必须来自同一次 React.createContext 调用(可以用 hack 方式解决此“局限”),仿佛 React 团队对于此特性的发展方向设计主要体现在小型状态管理上。如果需要实现更加灵活和直接的操作,Redux 也许会是更好的选择。
其次,Redux 丰富的生态以及中间件等机制,决定了其在很大程度上具有不可替代性。毕竟,已经使用 Redux 的项目,迁移成本也将是极大的,至少需要开发中先升级 React 以支持新版 context 吧。
最后,Redux 作为一个“发布订阅系统”,完全可以脱离 React 而单独存在,这样的基因也决定了其后天与 React 本身 context 不同的性征。
我认为,新版 React context 是对 React 本身“短板”的长线补充和完善,未来大概率也会有所打磨调整。Redux 也会进行一系列迭代,但就如同这次版本升级一样,将趋于稳定,更多的是细节上调整。
退一步讲,React context 的确也和 Redux 有千丝万缕的联系。任何类库或者框架都具有其短板,Redux 同样也如此。我们完全可以使用新版 React context,在使用层面来规避 Redux 的一些劣势,模仿 Redux 所能做到的一切。如同 didierfranc 的 react-waterfall,国内@方正的 Rectx,都是基于新版 React context 的解决方案。
最后,我很赞同@诚身所说:
选择用什么样的工具从来都不是决定一个开发团队成败的关键,根据业务场景选择恰当的工具,并利用工具反过来约束开发者,最终达到控制整体项目复杂度的目的,才是促进一个开发团队不断提升的核心动力。
没错,真正对项目起到决定性作用的还是是开发者本身,完善基础知识,提升开发技能,让我们从 Redux 4.0 的改动看起吧。
广告时间:
如果你对前端发展,尤其对 React 技术栈感兴趣:我的新书中,也许有你想看到的内容。关注作者 Lucas HC,新书出版将会有送书活动。
Happy Coding!
PS: 作者 Github仓库 和 知乎问答链接 欢迎各种形式交流!
我的其他几篇关于React技术栈的文章:
从setState promise化的探讨 体会React团队设计思想
探索 Redux4.0 版本迭代 论基础谈展望(对比 React context)的更多相关文章
- Excelize 发布 2.6.0 版本,功能强大的 Excel 文档基础库
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...
- Excelize 发布 2.3.0 版本, Go 语言 Excel 文档基础库
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...
- Excelize 发布 2.2.0 版本, Go 语言 Excel 文档基础库
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...
- Slickflow.NET 开源工作流引擎基础介绍(九) -- .NET Core2.0 版本实现介绍
前言:.NET Core 是.NET Framework的新一代版本,是微软开发的第一个跨平台 (Windows.Mac OSX.Linux) 的应用程序开发框架(Application Framew ...
- Lucene7.1.0版本的索引创建与查询以及维护,包括新版本的一些新特性探索!
一 吐槽 lucene版本更新实在太快了,往往旧版本都还没学会,新的就出来,而且每个版本改动都特别大,尤其是4.7,6,6,7.1.......ε=(´ο`*)))唉,但不可否认,新版本确实要比旧版本 ...
- Slickflow.NET 开源工作流引擎基础介绍-.NET Core2.0 版本实现介绍 (转)
前言:.NET Core 是.NET Framework的新一代版本,是微软开发的第一个跨平台 (Windows.Mac OSX.Linux) 的应用程序开发框架(Application Framew ...
- TensorFlow 2.0 深度学习实战 —— 浅谈卷积神经网络 CNN
前言 上一章为大家介绍过深度学习的基础和多层感知机 MLP 的应用,本章开始将深入讲解卷积神经网络的实用场景.卷积神经网络 CNN(Convolutional Neural Networks,Conv ...
- Nacos发布0.5.0版本,轻松玩转动态 DNS 服务
阿里巴巴微服务开源项目Nacos于近期发布v0.5.0版本,该版本主要包括了DNS-basedService Discovery,对Java 11的支持,持续优化Nacos产品用户体验,更深度的与Sp ...
- Apache Flink 1.9.0版本新功能介绍
摘要:Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台,它能够基于同一个Flink运行时,提供支持流处理和批处理两种类型应用的功能.目前,Apache Flink 1.9 ...
随机推荐
- 吴裕雄--天生自然python机器学习:使用决策树预测隐形眼镜类型
解决策树如何预测患者需要佩戴的隐形眼镜类型.使用小数据 集,我们就可以利用决策树学到很多知识:眼科医生是如何判断患者需要佩戴的镜片类型:一旦 理解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴 ...
- Invert Binary Tree(easy)
1.直接把递归把左右子树翻转即可 AC代码: /** * Definition for a binary tree node. * struct TreeNode { * int val; * Tre ...
- Ubuntu 12.04 查看DOCSIS配置文件
查看BIN格式的DOCSIS配置文件,需要专用工具. 工具安装 在Windows下,可以使用工具VultureWare DOCSIS Config Editor. 在Ubuntu(本机版本12.04) ...
- 88)PHP,PDOStatement对象
PDOStatement类,称之为PDO语句对象,SQL执行完(处理完)产生的结果对象. fetchColumn(index=) 允许传递参数,表示获得第一条记录的第几个字段的值. 相当于 getOn ...
- 让一个div拖动和让一个panel拖动加拉大拉小
一.让一个div拖动 <!doctype html> <html xmlns="http://www.w3.org/1999/xhtml"> <hea ...
- JavaScript学习总结(八)正则表达式
转自:http://segmentfault.com/a/1190000000699097 基本概念 正则表达式是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为“元字符”) ...
- java操作telnet远程登录
import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import jav ...
- [洛谷P3391] 文艺平衡树 (Splay模板)
初识splay 学splay有一段时间了,一直没写...... 本题是splay模板题,维护一个1~n的序列,支持区间翻转(比如1 2 3 4 5 6变成1 2 3 6 5 4),最后输出结果序列. ...
- 吴裕雄--天生自然 R语言开发学习:使用ggplot2进行高级绘图(续二)
#----------------------------------------------------------# # R in Action (2nd ed): Chapter 19 # # ...
- Lego:美团点评接口自动化测试实践
概述 接口自动化概述 众所周知,接口自动化测试有着如下特点: 低投入,高产出. 比较容易实现自动化. 和UI自动化测试相比更加稳定. 如何做好一个接口自动化测试项目呢? 我认为,一个“好的”自动化 ...