React 应用中的性能隐患 —— 神奇的多态

React 应用中的性能隐患 —— 神奇的多态

基于 React 框架的现代 web 应用常常经过不可变数据结构来管理它们的状态。好比使用比较知名的 Redux 状态管理工具。这种模式有许多优势而且即便在 React/Redux 生态圈外也愈来愈流行。html

这种机制的核心被称做为 reducers。 它们是一些能根据一个特定的映射行为 action(例如对用户交互的响应)把应用从一个状态映射到下一个状态的函数。经过这种核心抽象的概念,复杂的状态和 reducers 能够由一些更简单状态和 reducers 组成,这使得它易于对各部分代码隔离作单元测试。咱们仔细分析一下 Redux 文档 中的例子。前端

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
  }
}
复制代码

这个名叫 todo 的 reducer 根据给定的 action 把一个已有的 state 映射到了一个新的状态。这个状态就是一个普通的 JavaScript 对象。咱们单从性能角度来看这段代码,他彷佛是符合单态法则的,好比这个对象的形状(key/value)保持一致。react

const s1 = todo({}, {
  type: 'ADD_TODO',
  id: 1,
  text: "Finish blog post"
});

const s2 = todo(s1, {
  type: 'TOGGLE_TODO',
  id: 1
});

function render(state) {
  return state.id + ": " + state.text;
}

render(s1);
render(s2);
render(s1);
render(s2);
复制代码

表面上来看, render 中访问属性应该是单态的,好比说 state 对象应该有相同的对象形状- map 或者 V8 概念中的 hidden class 形式 — 无论何时, s1s2 都拥有 id, textcompleted 属性而且它们有序。然而,当经过 d8 运行这段代码并跟踪代码的 ICs (内联缓存) 时,咱们发现那个 render 表现出来的对象形状不相同, state.idstate.text 的获取变成了多态形式:android

那么问题来了,这个多态是从哪里来的?它确实表面看上去一致但其实有微小差别,咱们得从 V8 是如何处理对象字面量着手分析。V8 里,每一个对象字面量 (好比 {a:va,...,z:vb} 形式的表达形式 ) 定义了一个初始的map (map 在 V8 概念中特指对象的形状)这个 map 会在以后属性变更时迁移成其余形式的 map。因此,若是你使用一个空对象字面量 {} 时,这棵迁移树(transition tree)的根是一个不包含任何属性的 map,但若是你使用 {id:id, text:text, completed:completed} 形式的对象字面量,那么这个迁移树(transition tree)的根就会是一个包含这三个属性,让咱们来看一个精简过的例子:ios

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.js 运行命令后面加上 --allow-natives-syntax 跑这段代码(开启便可应用内部方法 %HaveSameMap),举个例子:git

尽管 a and b 这两个对象看上去是同样的 —— 依次拥有相同类型的属性,它们 map 结构并不同。缘由是它们的迁移树(transition tree)并不相同,咱们能够看如下的示例来解释:github

因此当对象初始化期间被分配不一样的对象字面量时,迁移树(transition tree)就不一样,map 也就不一样,多态就隐含的造成了。这一结论对你们广泛用的 Object.assign也适用,好比:web

let a = {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));
复制代码

这段代码仍是产生了不一样的 map ,由于对象 b 是从一个空对象( {} 字面量) 建立的,而属性是等到Object.assign 才给他分配。redux

这也代表,当你使用 spread (拓展运算符)处理属性,而且经过 Babel 来语法转译,就会遇到这个多态的问题。由于 Babel (其余转译器可能也同样), 对 spread 语法使用了 Object.assign 处理。后端

有一种方法能够避免这个问题,就是始终使用 Object.assign ,而且全部对象从一个空的对象字面量开始。可是这也会致使这个状态管理逻辑存在性能瓶颈:

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));
复制代码

不过,当一些代码变成多态也不意味着一切完了。对大部分代码而言,单态仍是多态并没啥关系。你应该在决定优化时多思考优化的价值。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索