React 應用中的效能隱患 —— 神奇的多型

candy_zheng發表於2018-03-12

React 應用中的效能隱患 —— 神奇的多型

基於 React 框架的現代 web 應用經常通過不可變資料結構來管理它們的狀態。比如使用比較知名的 Redux 狀態管理工具。這種模式有許多優點並且即使在 React/Redux 生態圈外也越來越流行。

這種機制的核心被稱作為 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)保持一致。

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 的獲取變成了多型形式:

React 應用中的效能隱患 —— 神奇的多型

那麼問題來了,這個多型是從哪裡來的?它確實表面看上去一致但其實有微小差異,我們得從 V8 是如何處理物件字面量著手分析。V8 裡,每個物件字面量 (比如 {a:va,...,z:vb} 形式的表達形式 ) 定義了一個初始的map (map 在 V8 概念中特指物件的形狀)這個 map 會在之後屬性變動時遷移成其他形式的 map。所以,如果你使用一個空物件字面量 {} 時,這棵遷移樹(transition tree)的根是一個不包含任何屬性的 map,但如果你使用 {id:id, text:text, completed:completed} 形式的物件字面量,那麼這個遷移樹(transition tree)的根就會是一個包含這三個屬性,讓我們來看一個精簡過的例子:

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),舉個例子:

React 應用中的效能隱患 —— 神奇的多型

儘管 a and b 這兩個物件看上去是一樣的 —— 依次擁有相同型別的屬性,它們 map 結構並不一樣。原因是它們的遷移樹(transition tree)並不相同,我們可以看以下的示例來解釋:

React 應用中的效能隱患 —— 神奇的多型

所以當物件初始化期間被分配不同的物件字面量時,遷移樹(transition tree)就不同,map 也就不同,多型就隱含的形成了。這一結論對大家普遍用的 Object.assign也適用,比如:

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 才給他分配。

React 應用中的效能隱患 —— 神奇的多型

這也表明,當你使用 spread (擴充運算子)處理屬性,並且通過 Babel 來語法轉譯,就會遇到這個多型的問題。因為 Babel (其他轉譯器可能也一樣), 對 spread 語法使用了 Object.assign 處理。

React 應用中的效能隱患 —— 神奇的多型

有一種方法可以避免這個問題,就是始終使用 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章