ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

React 多态性精读:Redux 不可变状态为何会阻断 V8 引擎的 Shapes 优化

React 多态性精读:Redux 不可变状态为何会阻断 V8 引擎的 Shapes 优化 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇精读对应周刊第 63 期主题源自对《Surprising Polymorphism in React Applications》一文的解读。文章将 JS 引擎层的 Shapes形状优化与 Redux 不可变immutable状态更新机制放在同一视角下审视每次 dispatch 生成的新 store 对象为什么常常无法被 V8 引擎优化读完本文你将理解多态polymorphism在 JS 引擎语境下的真实含义、Object.assign与解构语法对引擎优化的潜在影响以及什么时候才值得为此引入 immutable-js 这类库。1 引言为什么React 的多态性其实在讲 Redux原文标题是Surprising Polymorphism in React Applications但通读全文可以发现整篇文章都在围绕 Redux 展开reducer 返回新状态、dispatch 生成新的 store 树而这些正是 React 应用中最典型、也最频繁的对象生成场景。Redux 的使用场景并不局限于 React它同样可以配合 Vue、原生 JS 或任何视图层使用因此把文章标题理解成Redux 的多态性反而更妥当。这里的多态性不是面向对象里常见的继承/接口多态而是JS 引擎层面的对象结构多态结构完全相同的多个对象因为初始化方式不同被引擎拆分成多个互不共享的 Shape从而失去被引擎优化的机会。2 背景Redux immutable 特性与引擎优化的矛盾Redux 的核心约束之一是状态不可变每次 dispatch 一个 actionreducer 必须返回一个新的 state 对象而不是在原对象上修改。先看一个非常普通的 reducerconst 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; } };简化使用场景基于这个 reducertodo依次生成两个新 stores1、s2const 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 优化建议先阅读上一期精读 前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md。也就是说全局 store——最需要被优化的对象——在生成新 store 时可能无法被浏览器优化。这个问题很容易被忽视但影响不小。3 引擎原理铺垫Shapes 与相同结构对象共享 Shape在深入问题之前先回顾上一篇精读建立的核心概念ShapeV8 中称为 MapJS 引擎将对象的结构属性名与属性在内存中的偏移量与数据分离存储。结构相同的对象共享同一个 Shape从而大幅降低存储冗余并让属性访问变得更快。Transition chains / Transition trees当对象通过逐步赋值添加属性时引擎会为每一步生成新的 Shape形成链式chains或分叉trees的 Shape 结构。从一个空对象开始、按相同顺序添加相同属性会走同一条 transition 链最终得到同一个 Shape。Inline Caches引擎在get_by_id这类字节码指令中缓存对象的 Shape 与属性偏移量下次遇到相同 Shape 的对象直接命中缓存跳过查找过程。关键结论来自上一篇精读只有以相同方式初始化的对象才可能共享 Shape。这一结论正是本篇多态性问题的理论根基。4 实验验证初始化方式如何影响 Shape 共享原文给出了一个可以直接在 Node 中运行的验证脚本transition-trees.js// 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执行--allow-natives-syntax用于开放%HaveSameMap这类 V8 原生调试函数%HaveSameMap判断两个对象是否共享同一个 Map即 V8 引擎中 Shape 的实现。实验结果是falseJS 引擎无法对a、b做 Shapes 优化因为二者初始化方式不同——a用对象字面量一次性创建b从空对象逐步赋值两条 transition 路径互不相交。4.1 Redux 常用写法 Object.assign 同样受阻再看 Redux 代码中最常用的Object.assignObject.assign({}, state, { completed: !state.completed });新对象以{}空对象作为最初状态JS 引擎会为新对象创建Empty Shape这与原对象已有id、text、completed等字段的 Shape 一定不同。于是每次 dispatch 生成的新 store都无法复用旧 store 的 ShapeInline Caches 也随之失效。4.2 ES6 解构语法的隐患顺带一提ES6 的解构语法也存在同样的问题在 babel 编译阶段{ ...state, completed: true }这类对象展开最终被解析为Object.assign调用因此最终落到引擎上的执行路径与Object.assign完全相同同样绕过了 Shape 共享。5 作者的建议统一用 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这次结果是true。原因在于a、b都从空对象出发且属性x、y、z的添加顺序完全一致两条 transition 路径合并为同一条最终共享同一个 Shape——尽管它们是完全不同的两个对象实例。这也回应了上一期精读的核心结论尽量以相同方式初始化对象因为这样会生成较少的 Shapes见 前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md。6 精读为什么 immutable 对象之间也需要优化一个常见的疑惑是s1和s2明明是不同的引用为什么引擎还要在它们之间做优化这恰恰是对 Shapes 优化的误解。引擎级别的 Shapes 优化就是针对不同引用的对象引擎把对象的结构Shape与数据分离后凡是结构相同的对象无论有多少个实例、引用是否相同都能共享同一个 Shape 描述、复用 Inline Cache 中的偏移量。对数组也是同样的道理详见 前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md 中关于数组Elements存储优化的部分。所以Redux 全局 store 这种高频率、同结构、多实例的对象是 Shapes 优化的理想目标但正因为 immutable 赋值普遍采用Object.assign或解构新对象总是从 Empty Shape 重新开始导致 Shape 无法复用优化落空。6.1 从多态到单态的重新定义把这一视角浓缩成术语多态polymorphism多个结构相同的对象被引擎拆分成了多个不同的 Shape。每一个新 Shape 都意味着一次结构缓存失效属性访问退化为较慢的查找路径。单态monomorphism这些对象可以被同一个 Shape 复用引擎得以用最快的路径访问属性。这一概念与 前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md 中多态函数的性能问题一节相互印证当函数或者对象存在多种类型参数时性能没什么优化但单态函数性能大幅提升所以尽量让对象内部属性单态是比较有用的。两者讲的其实是同一件事——引擎只对稳定的、可复用的结构形态做激进优化。7 方案对比Object.assign / 解构 / immutable-js7.1 为什么更推荐 immutable-js 这类库在精读部分笔者给出的倾向性结论是更推荐使用 immutable-js 这类库来操作 immutable 对象而不是 Object.assign。理由是封装库内部可能通过统一的对象初始化方式主动利用 JS 引擎的优化能力——例如对树状数据结构采用统一的节点创建路径让大量同构节点共享 Shape关于 immutable 数据结构的实现细节包括 Vector trie、Hash maps trie 与结构共享原理可参考 前沿技术/9.精读《Immutable 结构共享》.md。需要说明的是封装库内部可能利用引擎优化属于一种推断而非确定事实具体能否获得优化收益取决于库的实现方式与运行引擎。更稳妥的表述是immutable-js 的价值在于把频繁的复制操作转交给经过精心设计的持久化数据结构从而在逻辑层面减少大对象整体拷贝带来的开销——这正是 前沿技术/9.精读《Immutable 结构共享》.md 中 Object.assign 作用于大对象时速度成为瓶颈10 万属性级对象操作耗时 134ms 量级 所描述的场景。7.2 解构语法的定位笔者个人也有过从Object.assign到 Immutablejs 库、最后又回到解构新语法的经历。在对象层级不深的情况下解构语法完全可以代替 Immutablejs 库——代码更简洁、可读性更强且多数业务场景下性能并非瓶颈。但通过最近两篇精读62 期 Shapes 与 Inline Caches、63 期多态性的分析我们需要重新思考这种取舍带来的优缺点在 JS 运行环境中Object.assign的优化效率比 Immutablejs 库更低——因为它破坏了 Shape 共享而解构经 babel 转译后与Object.assign等价同样继承了这一缺点。8 是否值得立刻重构性能权衡的边界最后是务实层面的建议完全没有必要现在就开始重构。原因很明确这只是 JS 运行环境中很小一部分影响因素Shapes 优化只是引擎性能优化的冰山一角DOM 与样式的优化对前端性能影响往往更大见 前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md 的总结。优化是有成本的例如为了引入 Immutablejs可能让你的网络延时增加 100%新增依赖体积带来的加载开销这与省下的引擎优化收益相比未必划算。因此仅在有必要的时候优化它当确认 store 更新路径存在可测量的性能瓶颈、且对象结构在运行期高度稳定时才值得针对性的调整对象初始化方式或引入专用 immutable 库。9 与周刊其他主题的联动本期的核心价值在于把引擎原理与日常框架用法连接起来周刊内还有几篇可以串读前沿技术/62.精读《JS 引擎基础之 Shapes and Inline Caches》.md本篇的前置知识系统讲解 Shapes、Transition chains/trees、Inline Caches 与数组存储优化。前沿技术/22.精读《V8 引擎特性带来的的 JS 性能变化》.md从 V8 优化视角看多态函数与单态函数的性能差异。前沿技术/9.精读《Immutable 结构共享》.mdImmutablejs 的结构共享实现原理以及 Object.assign 与 Immutable 的性能边界对比。前沿技术/42.精读《前端数据流哲学》.md数据流方案的演进背景immer 通过 Proxy 元编程将 setter 重写为Object.assign()实现 mutable 到 immutable 的转换是另一种避免手写不可变赋值的思路。10 总结回到本期的主题多态polymorphism指的是多个相同结构的对象被拆分成了多个 Shape单态monomorphism指的是这些对象可以被一个 Shape 复用。Redux 的 immutable 特性天然需要高频创建同结构的新对象这本应是引擎优化的理想对象但Object.assign与解构语法babel 转译为Object.assign总是从 Empty Shape 重新开始使新对象与原对象无法共享 Shape导致全局 store 无法享受 Shapes 优化与 Inline Caches 的加速。最终结论可以收敛为三点认识问题明确多态性在引擎语境下指的是 Shape 无法复用而非传统面向对象的多态。权衡方案层级不深时解构语法可替代 Immutablejs但需要知道Object.assign系列写法在引擎优化效率上不如 Immutablejs 这类库。克制优化这只是引擎性能的一小块影响因素仅在确有瓶颈时再做针对性优化不必为理论收益引入额外依赖成本。原文的讨论地址为周刊 Issue #92《精读〈React 的多态性〉》周刊项目主页的发布辅助逻辑可参考仓库根目录的 helper.js。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Mortal日本麻将AIRust驱动的强化学习实战解析Mortal日本麻将AIRust驱动的强化学习实战解析 在AI竞技领域日本麻将因其复杂的决策空间和不确定性而成为强化学习的理想测试平台。Mortal项目正是人工智能强化学习深度学习AI 应用react-jsonschema-form与Redux不可变状态实践react jsonschema form与Redux不可变状态实践 痛点表单状态与Redux的矛盾 你是否曾在使用react jsonschema form前端UI组件react-redux-typescript-guide不可变性Type-level Immutability实现状态安全react redux typescript guide不可变性Type level Immutability实现状态安全 你是否在ReactRedux项目前端教程上一篇FastScriptReload调试完全指南如何在热重载代码中设置断点下一篇终极Safari兼容性测试指南让你的前端项目覆盖99%用户创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表