一、痛點
在我們的印象中,React
好像就意味著元件化、高效能,我們永遠只需要關心資料整體,兩次資料之間的 UI
如何變化,則完全交給 React Virtual Dom
的 Diff 演算法
去做。以至於我們很隨意的去操縱資料,基本優化shouldComponentUpdate
也懶得去寫,畢竟不寫也能正確渲染。但隨著應用體積越來越大,會發現頁面好像有點變慢了,特別是元件巢狀比較多,資料結構比較複雜的情況下,隨便改變一個表單項,或者對列表做一個篩選都要耗時 100ms
以上,這個時候我們就需要優化了!當然如果沒有遇到效能瓶頸,完全不用擔心,過早優化是邪惡的。這裡我們總結一個很簡單的方案來讓 React
應用效能發揮到極致。在下面一部分,我們先回顧一下一些背景知識,包括:JavaScript
變數型別和 React
渲染機制,如果你是老鳥可以直接跳過。
二、一些背景知識的回顧
1. 變數型別
JavaScript的變數型別有兩類:
- 基本型別:6 種基本資料型別,
Undefined
、Null
、Boolean
、Number
、String
、Symbol
- 引用型別:統稱為
Object
型別,細分為:Object
型別、Array
型別、Date
型別、RegExp
型別、Function
型別等。
舉個例子:
let p1 = { name: 'neo' };
let p2 = p1;
p2.name = 'dave';
console.log(p1.name); // dave複製程式碼
在引用型別裡,宣告一個 p1
的物件,把 p1
賦值給 p2
,此時賦的其實是該物件的在堆中的地址,而不是堆中的資料,也就是兩個變數指向的是同一個儲存空間,後面 p2.name
改變後,也就影響到了 p1
。雖然這樣做可以節約記憶體,但當應用複雜後,就需要很小心的運算元據了,因為一不注意修改一個變數的值可能就影響到了另外一個變數。如果我們想要讓他們不互相影響,就需要拷貝出一份一模一樣的資料,拷貝又分淺拷貝與深拷貝,淺拷貝只會拷貝第一層的資料,深拷貝則會遞迴所有層級都拷貝一份,比較消耗效能。
2. React
在 React
中,每次 setState
, Virtual DOM
會計算出前後兩次虛擬 DOM
物件的區別,再去修改真實需要修改的 DOM
。由於 js
計算速度很快,而操作真實 DOM
相對比較慢,Virtual DOM
避免了沒必要的真實 DOM
操作,所以 React
效能很好。但隨著應用複雜度的提升, DOM
樹越來越複雜,大量的對比操作也會影響效能。比如一個 Table
元件,修改其中一行 Tr
元件的某一個欄位, setState
後,其他所有行 Tr
元件也都會執行一次 render
函式,這其實是不必要的。我們可以通過 shouldComponentUpdate
函式決定是否更新元件。大部分時候我們是可以知道哪些元件是不會變的,根本就沒必要去計算那一部分虛擬 DOM
。
三、 PureComponent
React15.3
中新加了一個類PureComponent,前身是 PureRenderMixin
,和 Component
基本一樣,只不過會在 render
之前幫元件自動執行一次shallowEqual(淺比較),來決定是否更新元件,淺比較類似於淺複製,只會比較第一層。使用 PureComponent
相當於省去了寫 shouldComponentUpdate
函式,當元件更新時,如果元件的 props
和 state
:
- 引用和第一層資料都沒發生改變,
render
方法就不會觸發,這是我們需要達到的效果。 - 雖然第一層資料沒變,但引用變了,就會造成虛擬
DOM
計算的浪費。 - 第一層資料改變,但引用沒變,會造成不渲染,所以需要很小心的運算元據。
四、 Immutable.js
Immutable.js是 Facebook
在 2014
年出的永續性資料結構的庫,永續性指的是資料一旦建立,就不能再被更改,任何修改或新增刪除操作都會返回一個新的 Immutable
物件。可以讓我們更容易的去處理快取、回退、資料變化檢測等問題,簡化開發。並且提供了大量的類似原生 JS
的方法,還有 Lazy Operation
的特性,完全的函數語言程式設計。
import { Map } from "immutable";
const map1 = Map({ a: { aa: 1 }, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1 !== map2; // true
map1.get('b'); // 2
map2.get('b'); // 50
map1.get('a') === map2.get('a'); // true複製程式碼
可以看到,修改 map1
的屬性返回 map2
,他們並不是指向同一儲存空間,map1
宣告瞭只有,所有的操作都不會改變它。
ImmutableJS
提供了大量的方法去更新、刪除、新增資料,極大的方便了我們操縱資料。除此之外,還提供了原生型別與 ImmutableJS
型別判斷與轉換方法:
import { fromJS, isImmutable } from "immutable";
const obj = fromJS({
a: 'test',
b: [1, 2, 4]
}); // 支援混合型別
isImmutable(obj); // true
obj.size(); // 2
const obj1 = obj.toJS(); // 轉換成原生 `js` 型別複製程式碼
ImmutableJS
最大的兩個特性就是: immutable data structures
(永續性資料結構)與 structural sharing
(結構共享),永續性資料結構保證資料一旦建立就不能修改,使用舊資料建立新資料時,舊資料也不會改變,不會像原生 js
那樣新資料的操作會影響舊資料。而結構共享是指沒有改變的資料共用一個引用,這樣既減少了深拷貝的效能消耗,也減少了記憶體。比如下圖:
左邊是舊值,右邊是新值,我需要改變左邊紅色節點的值,生成的新值改變了紅色節點到根節點路徑之間的所有節點,也就是所有青色節點的值,舊值沒有任何改變,其他使用它的地方並不會受影響,而超過一大半的藍色節點還是和舊值共享的。在 ImmutableJS
內部,構造了一種特殊的資料結構,把原生的值結合一系列的私有屬性,建立成 ImmutableJS
型別,每次改變值,先會通過私有屬性的輔助檢測,然後改變對應的需要改變的私有屬性和真實值,最後生成一個新的值,中間會有很多的優化,所以效能會很高。
五、 案例
首先我們看看只使用 React
的情況下,應用效能為什麼會被浪費,程式碼地址:github.com/wulv/fe-exa… ,這個案例使用 create-react-app
,檢測工具使用 chrome
外掛:React Perf。執行
git clone https://github.com/wulv/fe-example.git
cd fe-example/react-table
yarn
yarn start複製程式碼
可以開啟頁面,開始記錄,然後隨便對一列資料進行修改,結束記錄,可以看到我們僅修改了一行資料,但在 Print Wasted
那一項裡,渲染 Tr
元件浪費了5次:
無論是新增,刪除操作,都會浪費
n-1
次 render
,因為 App
元件的整個 state
改變了,所有的元件都會重新渲染一次,最後對比出需要真實 DOM
的操作。我們把 Table
元件和 Tr
繼承的 Component
改成 PureComponent
,那麼, Tr
元件每次更新都會進行一次 shallowEqual
比較,在記錄一次,會發現修改操作沒有了浪費,然而這個時候新增和刪除操作卻無效了,分析一下新增的操作是:
add = () => {
const { data } = this.state;
data.push(dataGenerate())
this.setState({
data
})
}複製程式碼
data.push
並沒有改變 data
的引用,所以 PureComponent
的 shallowEqual
直接返回了 true
,不去 render
了。這並不是我們想要的,所以如果使用 Component
必定帶來效能浪費,使用 PureComponent
又必須保證元件需要更新時,props
或 state
返回一個新引用,否則不會更新 UI
。
這個時候, ImmutableJS
就可以顯示出它的威力了,因為它可以保證每次修改返回一個新的 Object
,我們看看修改後的例子:程式碼地址:github.com/wulv/fe-exa… ,執行上面例子同樣的操作,可以看到:
新增,刪除,修改操作,沒有一次浪費。沒有浪費的原因是所有的子元件都使用了
PureComponent
, ImmutableJS
保證修改操作返回一個新引用,並且只修改需要修改的節點(PureComponent
可以渲染出新的改動),其他的節點引用保持不變(PureComponent
直接不渲染)。可以看出, PureComponent
與 ImmutableJS
簡直是天生一對啊,如果結合 redux
,那就更加完美了。因為 redux
的 reducer
必須每次返回一個新的引用,有時候我們必須使用 clone
或者 assign
等操作來確保返回新引用,使用 ImmutanleJS
天然保證了這一點,根本就不需要 lodash
等函式庫了,比如我使用redux + immutable + react-router + express
寫了一個稍微複雜點的例子:github.com/wulv/fe-exa… pageIndex
的 store
的狀態是:
{
loading: false,
tableData: [{
"name": "gyu3w0oa5zggkanciclhm2t9",
"age": 64,
"height": 121,
"width": 71,
"hobby": {
"movie": {
"name": "zrah6zrvm9e512qt4typhkt9",
"director": "t1c69z1vd4em1lh747dp9zfr"
}
}
}],
totle: 0
}複製程式碼
如果我需要快速修改 width
的值為90,比較一下使用深拷貝、 Object.assign
和 ImmutableJS
三種方式的區別:
// payload = { name: 'gyu3w0oa5zggkanciclhm2t9', width: 90 }
// 1. 使用深拷貝
updateWidth(state, payload) {
const newState = deepClone(state);
return newState.tableData.map(item => {
if (tem.name === payload.name) {
item.width = payload.width;
}
return item;
});
}
// 2. 使用Object.assign
updateWidth(state, payload) {
return Object.assign({}, state, {
tableData: state.state.map(item => {
if (item.name === payload.name) {
return Object.assign({}, item, { width: payload.width });
}
return item;
})
})
}
// 3. 使用ImmutableJS
updateWidth(state, payload) {
return state.update('tableData', list => list.update(
list.findIndex((item) => item.get('name') === payload.name),
item => item.set('width', payload.width)));
}複製程式碼
使用深拷貝是一個昂貴的操作,而且引用都改變了,必然造成 re-render
, 而 Object.assign
會淺複製第一層,雖然不會造成 re-render
,但淺複製把其他的屬性也都複製了一次,在這裡也是很沒有必要的,只有使用 ImmutableJS
完美的完成了修改,並且程式碼也最少。
六、 優勢與不足
可以看出, ImmutableJS
結合 PureComponent
可以很大程度的減少應用 re-render
的次數,可以大量的提高效能。但還是有一些不足的地方:
- 獲取元件屬性必須用
get
或getIn
操作(除了Record
型別),這樣和原生的.
操作比起來就麻煩多了,如果元件之前已經寫好了,還需要大量的修改。 ImmutableJS
庫體積比較大,大概56k,開啟gzip
壓縮後16k。- 學習成本。
- 難以除錯,在
redux-logger
裡面需要在stateTransformer
配置裡執行state.toJS()
。
七、 最佳實踐
其實,重要的是程式設計者需要有效能優化的意識,熟悉 js
引用型別的特性,瞭解事情的本質比會使用某個框架或庫更加重要。用其他的方法也是完全可以達到 ImmutableJS
的效果,比如新增資料可以使用解構操作符的方式:
add = () => {
const { data } = this.state;
this.setState({
data: [...data, dataGenerate()]
})
}複製程式碼
只不過如果資料巢狀比較深,寫起來還是比較麻煩。以下有一些小技巧:
- 還有兩個輕量庫可以實現不可變資料結構:seamless-immutable或者immutability-helper,只不過原理完全不一樣,效率也沒那麼高。
- 避免大量使用
toJS
操作,這樣會浪費效能。 - 不要將簡單的
JavaScript
物件與Immutable.JS
混合 - 結合
redux
的時候,要使用import { combineReducers } from 'redux-immutablejs';
,因為redux
的combineReducers
期望state
是一個純淨的js
物件。 - 儘量將
state
設計成扁平狀的。 - 展示元件不要使用
Immutable
資料結構。 - 不要在
render
函式裡一個PureComponent
元件的props
使用bind(this)
或者style={ { width: '100px' } }
,因為shallowEqual
一定會對比不通過。
八、 參考連結
- Immutable.js, persistent data structures and structural sharing
- immutable.js is much faster than native javascript
- Immutable 詳解及 React 中實踐
本文首發於有贊技術部落格。