精读《React 的多态性》
1 引言
本周精读的文章是:surprising-polymorphism-in-react-applications,看看作者是如何解释这个多态性含义的。
读完文章才发现,文章标题改为 Redux 的多态性更妥当,因为整篇文章都在说 Redux,而 Redux 使用场景不局限于 React。
2 概述
Redux immutable 特性可能产生浏览器无法优化的性能问题,也就是浏览器无法做 shapes 优化,也就是上一篇精读《JS 引擎基础之 Shapes and Inline Caches》 里提到的。
先看看普通的 redux 的 reducer:
const todo = (state = {}, action) => {
switch (action.type) {
case "ADD_TODO":
return {
id: action.id,
text: action.text,
completed: false
};
case "TOGGLE_TODO":
if (state.id !== action.id) {
return state;
}
return Object.assign({}, state, {
completed: !state.completed
});
default:
return state;
}
};
我们简化一下使用场景,假设基于这个 reducer todo
,生成了两个新 store s1
s2
:
const s1 = todo(
{},
{
type: "ADD_TODO",
id: 1,
text: "Finish blog post"
}
);
const s2 = todo(s1, {
type: "TOGGLE_TODO",
id: 1
});
看上去很常见,也的确如此,我们每次 dispatch 都会根据 reducer 生成新的 store 树,而且是一个新的对象。然而对 js 引擎而言,这样的代码可能做不了 Shapes 优化(关于 Shapes 优化建议阅读上一期精读 Shapes 优化),也就是最需要做优化的全局 store,在生成新 store 时无法被浏览器优化,这个问题很容易被忽视,但的确影响不小。
至于为什么会阻止 js 引擎的 shapes 优化,看下面的代码:
// transition-trees.js
let a = {x:1, y:2, z:3};
let b = {};
b.x = 1;
b.y = 2;
b.z = 3;
console.log("a is", a);
console.log("b is", b);
console.log("a and b have same map:", %HaveSameMap(a, b));
通过 node --allow-natives-syntax test.js
执行,通过调用 node 原生函数 %HaveSameMap
判断这种情况下 a
与 b
是否共享一个 shape(v8 引擎的 Shape 实现称为 Map)。
结果是 false
,也就是 js 引擎无法对 a
b
做 Shapes 优化,这是因为 a
与 b
对象初始化的方式不同。
同样,在 Redux 代码中常用的 Object.assign
也有这个问题:
因为新的对象以 {}
空对象作为最初状态,js 引擎会为新对象创建 Empty Shape,这与原对象的 Shape 一定不同。
顺带一提 es6 的解构语法也存在同样的问题,因为 babel
将解构最终解析为 Object.assign
:
对这种尴尬的情况,作者的建议是对所有对象赋值时都是用 Object.assign
以保证 js 引擎可以做 Shapes 优化:
let a = Object.assign({}, {x:1, y:2, z:3});
let b = Object.assign({}, a);
console.log("a is", a);
console.log("b is", b);
console.log("a and b have same map:", %HaveSameMap(a, b)); // true
3 精读
这篇文章需要与上一篇 精读《JS 引擎基础之 Shapes and Inline Caches》 连起来看容易理解。
作者描述的性能问题是引擎级别的 Shapes 优化问题,读过上篇精读就很容易知道,只有相同初始化方式的对象才被 js 引擎做优化,而 Redux 频繁生成的 immutable 全局 store 是否能被优化呢?答案是“往往不能”,因为 immutable 赋值问题,我们往往采用 Object.assign
或者解构方式赋值,这种方式产生的新对象与原对象的 Shape 不同,导致 Shape 无法复用。
这里解释一下疑惑,为什么说 immutable 对象之间也要优化呢?这不是两个不同的引用吗?这是因为 js 引擎级别的 Shapes 优化就是针对不同引用的对象,将对象的结构:Shape 与数据分离开,这样可以大幅优化存储效率,对数组也一样,上一篇精读有详细介绍。
所以笔者更推荐使用比如 immutable-js 这种库操作 immutable 对象,而不是 Object.assign,因为封装库内部是可能通过统一对象初始化方式利用 js 引擎进行优化的。
4 总结
原文提到的多态是指多个相同结构对象,被拆分成了多个 Shape;而单态是指这些对象可以被一个 Shape 复用。
笔者以前也经历过从 Object.assign
到 Immutablejs 库,最后又回到解构新语法的经历,觉得在层级不深情况下解构语法可以代替 Immutablejs 库。
通过最近两篇精读的分析,我们需要重新思考这样做带来的优缺点,因为在 js 环境中,Object.assign
的优化效率比 Immutablejs 库更低。
最后,也完全没必要现在就开始重构,因为这只是 js 运行环境中很小一部分影响因素,比如为了引入 Immutablejs 让你的网络延时增加了 100%?所以仅在有必要的时候优化它。
5 更多讨论
如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。