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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索