- 原文地址:Surprising polymorphism in React applications
- 原文作者:Benedikt Meurer
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者: Candy Zheng
- 校對者:goldEli,老教授
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 形式 — 不管什麼時候, s1
和 s2
都擁有 id
, text
和 completed
屬性並且它們有序。然而,當通過 d8
執行這段程式碼並跟蹤程式碼的 ICs
(內聯快取) 時,我們發現那個 render
表現出來的物件形狀不相同, state.id
和 state.text
的獲取變成了多型形式:
那麼問題來了,這個多型是從哪裡來的?它確實表面看上去一致但其實有微小差異,我們得從 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
),舉個例子:
儘管 a
and b
這兩個物件看上去是一樣的 —— 依次擁有相同型別的屬性,它們 map 結構並不一樣。原因是它們的遷移樹(transition tree)並不相同,我們可以看以下的示例來解釋:
所以當物件初始化期間被分配不同的物件字面量時,遷移樹(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
才給他分配。
這也表明,當你使用 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));
複製程式碼
不過,當一些程式碼變成多型也不意味著一切完了。對大部分程式碼而言,單態還是多型並沒啥關係。你應該在決定優化時多思考優化的價值。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。