探索 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 ...
随机推荐
- string 转化xml && xml转化为string
一.使用最原始的javax.xml.parsers,标准的jdk api // 字符串转XML [java] view plaincopyprint? String xmlStr = \". ...
- 常见的Java不规范代码
1.格式化源代码 Ctrl + Shift + F – 格式化源代码. Ctrl + Shift + O – 管理import语句并移除未使用的语句 除了手动执行这两个功能外,你还可以让Eclipse ...
- 奇点云数据中台技术汇(五)| CDP,线下零售顾客运营中台
顾客数据平台(Customer Data Platform,简称CDP),是近年兴起的一种以顾客为核心.聚焦客群细分与人群洞察的企业数据应用平台. 听上去很互联网啊?跟实体行业和零售营销有什么关系呢? ...
- C#阶段小结
一.数据类型: (一)内建类型: 整型(int ,short, long ,byte ,uint ,ushort, ulong ,sbyte): 浮点型(double float decimal): ...
- 简化Java编程的法宝,让工作更高效
如果你没有看过之前的文章,也不要紧,这并不影响你对接下来的内容的理解,不过为了照顾直接看到第二篇的同学,还是有必要介绍一下HuTool的引入方式. 在项目的pom.xml的dependencies中加 ...
- Python面向对象三大特征
继承 面向对象中的继承就是继承的类直接拥有被继承类的属性而不需要在自己的类体中重新再写一遍,其中被继承的类叫做父类.基类,继承的类叫做派生类.子类.在python3中如果不指定继承哪个类,默认就会继承 ...
- win10安装CAD失败,怎么强力卸载删除注册表并重新安装
一些搞设计的朋友在win10系统下安装CAD失败或提示已安装,也有时候想重新安装CAD的时候会出现本电脑windows系统已安装CAD,你要是不留意直接安装CAD,只会安装CAD的附件或者直接提示失败 ...
- MAYA 卸载工具,完美彻底卸载清除干净maya各种残留注册表和文件
是不是遇到MAYA/CAD/3DSMAX/INVENTOR安装失败?AUTODESK系列软件着实令人头疼,MAYA/CAD/3DSMAX/INVENTOR安装失败之后不能完全卸载!!!(比如maya, ...
- 使用fastai完成图像分类
by Wenqi Sun 1 min read Categories Deep Learning Tags Fastai CNN Application 1. 使用现有数据集进行分类 图像数据为Oxf ...
- TortoiseSVN配置和使用教程
2009-04-24 来源:dev.idv.tw 1 安装及下载client 端 2 什么是SVN(Subversion)? 3 为甚么要用SVN? 4 怎么样在Windows下面建立SVN Repo ...