在咱們的印象中,React
好像就意味着組件化、高性能,咱們永遠只須要關心數據總體,兩次數據之間的 UI
如何變化,則徹底交給 React Virtual Dom
的 Diff 算法
去作。以致於咱們很隨意的去操縱數據,基本優化shouldComponentUpdate
也懶得去寫,畢竟不寫也能正確渲染。但隨着應用體積愈來愈大,會發現頁面好像有點變慢了,特別是組件嵌套比較多,數據結構比較複雜的狀況下,隨便改變一個表單項,或者對列表作一個篩選都要耗時 100ms
以上,這個時候咱們就須要優化了!固然若是沒有遇到性能瓶頸,徹底不用擔憂,過早優化是邪惡的。這裏咱們總結一個很簡單的方案來讓 React
應用性能發揮到極致。在下面一部分,咱們先回顧一下一些背景知識,包括:JavaScript
變量類型和 React
渲染機制,若是你是老鳥能夠直接跳過。javascript
JavaScript的變量類型有兩類:html
Undefined
、 Null
、 Boolean
、 Number
、 String
、 Symbol
Object
類型,細分爲:Object
類型、 Array
類型、 Date
類型、 RegExp
類型、 Function
類型等。舉個例子:java
let p1 = { name: 'neo' };
let p2 = p1;
p2.name = 'dave';
console.log(p1.name); // dave複製代碼
在引用類型裏,聲明一個 p1
的對象,把 p1
賦值給 p2
,此時賦的實際上是該對象的在堆中的地址,而不是堆中的數據,也就是兩個變量指向的是同一個存儲空間,後面 p2.name
改變後,也就影響到了 p1
。雖然這樣作能夠節約內存,但當應用複雜後,就須要很當心的操做數據了,由於一不注意修改一個變量的值可能就影響到了另一個變量。若是咱們想要讓他們不互相影響,就須要拷貝出一份如出一轍的數據,拷貝又分淺拷貝與深拷貝,淺拷貝只會拷貝第一層的數據,深拷貝則會遞歸全部層級都拷貝一份,比較消耗性能。react
在 React
中,每次 setState
, Virtual DOM
會計算出先後兩次虛擬 DOM
對象的區別,再去修改真實須要修改的 DOM
。因爲 js
計算速度很快,而操做真實 DOM
相對比較慢,Virtual DOM
避免了不必的真實 DOM
操做,因此 React
性能很好。但隨着應用複雜度的提高, DOM
樹愈來愈複雜,大量的對比操做也會影響性能。好比一個 Table
組件,修改其中一行 Tr
組件的某一個字段, setState
後,其餘全部行 Tr
組件也都會執行一次 render
函數,這實際上是沒必要要的。咱們能夠經過 shouldComponentUpdate
函數決定是否更新組件。大部分時候咱們是能夠知道哪些組件是不會變的,根本就不必去計算那一部分虛擬 DOM
。git
React15.3
中新加了一個類PureComponent,前身是 PureRenderMixin
,和 Component
基本同樣,只不過會在 render
以前幫組件自動執行一次shallowEqual(淺比較),來決定是否更新組件,淺比較相似於淺複製,只會比較第一層。使用 PureComponent
至關於省去了寫 shouldComponentUpdate
函數,當組件更新時,若是組件的 props
和 state
:github
render
方法就不會觸發,這是咱們須要達到的效果。DOM
計算的浪費。Immutable.js是 Facebook
在 2014
年出的持久性數據結構的庫,持久性指的是數據一旦建立,就不能再被更改,任何修改或添加刪除操做都會返回一個新的 Immutable
對象。可讓咱們更容易的去處理緩存、回退、數據變化檢測等問題,簡化開發。而且提供了大量的相似原生 JS
的方法,還有 Lazy Operation
的特性,徹底的函數式編程。web
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
類型判斷與轉換方法:chrome
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
那樣新數據的操做會影響舊數據。而結構共享是指沒有改變的數據共用一個引用,這樣既減小了深拷貝的性能消耗,也減小了內存。好比下圖:
express
左邊是舊值,右邊是新值,我須要改變左邊紅色節點的值,生成的新值改變了紅色節點到根節點路徑之間的全部節點,也就是全部青色節點的值,舊值沒有任何改變,其餘使用它的地方並不會受影響,而超過一大半的藍色節點仍是和舊值共享的。在 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()]
})
}複製代碼
只不過若是數據嵌套比較深,寫起來仍是比較麻煩。如下有一些小技巧:
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
必定會對比不經過。本文首發於有贊技術博客。