本文主要是針對性能優(yōu)化做介紹,對應的React本身性能就比較不錯!要介紹的點如下:
- 單個React組件的性能優(yōu)化
- 多個React組件的性能優(yōu)化
- 利用reselect提高數(shù)據(jù)選取的性能
因為現(xiàn)代化的Web應用的開發(fā)都是 組件化開發(fā)模式 ,因此我們不可能站在全局的角度去優(yōu)化,而是針對Web應用的核心部分,也就是 組件 針對性的進行優(yōu)化操作!
01|單個React組件性能優(yōu)化
-
Virtual DOM幫助React組件提高渲染性能
- 雖然每次都是重新渲染,但是利用React最核心的功能,DIFF機制進行diff之后 渲染該渲染的部分!
- 雖然說每次的DOM操作量比較少,但是計算和比較的時候依然是一個比較復雜的過程! 這樣一來就可以知道,在使用Virtual DOM之前就確認渲染結(jié)果不會有變化! 提升性能的方法不是diff而是不渲染甚至是不進行Diff!
對性能進行檢測,對對應的部分進行針對性的優(yōu)化!
避免過早優(yōu)化,我們應該對性能影響最關(guān)鍵的代碼進行優(yōu)化!
使用提供的 shouldComponentUpdate 手動操作組件是否渲染! 更加細粒度的控制組件的渲染邏輯!
react-redux提供的 shouldComponentUpdate 對比prop和上一次使用到的prop上面,用的盡量簡單的方法! 也只是對應的淺層比較,如果說對應的porp值是復雜的對象,只看是不是同一對象的引用,如果不是,哪怕這兩個對象的內(nèi)容完全一樣,也會被認為兩個不同的prop!
為什么不使用深層比較呢?
一個對象到底有多少層是無法預料的,如果使用遞歸對每個字段進行深層比較,讓代碼復雜的同時,也讓性能比較低下!
-
對應的props如果說是對象類型的話, 想讓react-redux認為前后的對象類型prop是相同的,就必須要保證只想的同一對象!
- 比如說是style屬性 可以使用一個對象對樣式進行設置,那么就可以將對應的 樣式進行抽象成一個變量進行使用!
對應的 關(guān)于事件的處理,如果說每次都是以箭頭函數(shù)的方式進行處理的話, 每次都是產(chǎn)生匿名函數(shù)! 匿名函數(shù)也就是每次都是新的函數(shù) 因此如果涉及到組件被應用到 多個頁面中內(nèi)存的占用就非常高!
對應的我們通過具體的代碼可以很清楚的復現(xiàn)這個知識點:
import React,{PropTypes} from "react";
import {toogleTodo,removeTodo} from "../actions";
const TodoList = ({todos,onToggleTodo,onRemoveTodo})=>{
return (
<ul className='todo-list'>
todos.map((item)=>{
<TodoItem key={item.id} text={item.text} completed={item.completed}
onToggle={()=>onToogleTodo(item.id)}
onRemove={()=>onRemoveTodo(item.id)} />
})
</ul>
);
}
對應的TodoItem點擊了對應的按鈕,調(diào)用父類的回調(diào)函數(shù),父類產(chǎn)生一個新的TodoItem 對應的每次更新都躲不過重新渲染的命運!
02|多個React組件的性能優(yōu)化
多個React組件的優(yōu)化,首先就需要學會從React的生命周期入手,從對應的生命周期階段針對性的去優(yōu)化!
render=>start: render method
op1=>operation: VDOM Tree
op2=>operation: RC or DOM
end=>end: View
action=>inputoutput: Action
render->op1->op2->end->action->op1
- render方法在內(nèi)存中產(chǎn)生了樹形的結(jié)構(gòu)(Virtual DOM)
- 樹上的每一個節(jié)點表示: React組件或者說是原生的DOM元素
- React 根據(jù) Virtual DOM渲染瀏覽器中的DOM樹!
如果說用戶的交互出發(fā)了頁面的更新,網(wǎng)頁中需要更新頁面的話,React 依然通過render方法獲得了一個樹形結(jié)構(gòu)VirtualDOM 這個時候不能完全和裝載過程一樣直接使用VirtualDOm去產(chǎn)生DOM樹!
這個階段(更新階段)巧妙的對比原有的Virtual DOM和新生成的Virtual DOM找出兩者的不同之處,根據(jù)不同來更新DOM! 只做必要的最小的改動!
對應的這個找不同的過程叫做 Reconciliation 調(diào)和!
對照計算機科學目前的算法研究成果,比對N個節(jié)點的樹形結(jié)構(gòu)的算法,時間復雜度是 如果說比對兩個100節(jié)點的DOM樹需要計算
次 其中的算法復雜度 N表示節(jié)點數(shù),
但是對應的React實際采用的算法需要的時間復雜度為 ,對比兩個樹形怎么著都要比對兩個樹形上的節(jié)點!
也不存在比復雜度更低的算法
對應的調(diào)和過程:
- 節(jié)點類型不同的情況
- React會直接丟棄原來樹形結(jié)構(gòu),然后重建DOM樹,對應的React組件也會經(jīng)歷卸載的生命周期
- 面對不同的應用場景,可能會引發(fā)樹結(jié)構(gòu)某些組件的卸載和裝載過程!
- 節(jié)點類型相同的情況
- 如果兩個數(shù)形結(jié)構(gòu)的根節(jié)點類型相同,React就認為原來的根節(jié)點只需要更新過程,不會將其卸載,也不會引發(fā)根節(jié)點的重新裝載!
- 對應的節(jié)點類型分為兩種:一種是DOM元素對應的也就是所謂的HTML元素,另一種則是 React的組件,也就是利用React庫定制的類型!
- 如果兩個數(shù)形結(jié)構(gòu)的根節(jié)點類型相同,React就認為原來的根節(jié)點只需要更新過程,不會將其卸載,也不會引發(fā)根節(jié)點的重新裝載!
對應的如果說屬性結(jié)構(gòu)的根節(jié)點不是DOM元素,那就只可能是React組件類型,那么React做的工作類似,React此時也不知道如何更新DOM樹,因此邏輯還在React組件之中,React能做的也就是通過新節(jié)點的props更新原來的組件實例,引發(fā)組件實例更新的過程! 按照順序觸發(fā):
- shouldComponentUpdate 如果不需要更新的話,可以在函數(shù)中直接返回false來保持最大的性能!
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
01|如果說多個子組件的情況是怎么樣的呢?
我們那一個最簡單的TodoItem來舉例吧:
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>
在更新之后,用JSX表示是這樣:
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
<TodoItem text="Thrid" completed={false} />
</ul>
對應的結(jié)果就是,react檢查多出了一個TodoItem,創(chuàng)建一個新的TodoItem組件實例,該實例需要經(jīng)歷裝載的過程,但是對于前面兩個TodoItem實例,React會引發(fā)他們的更新過程! 但是如果說對應的shouldComponentUpdate函數(shù)實現(xiàn)恰當,props檢查之后就返回false之后,可以避免實質(zhì)的更新操作!
- 剛剛那樣是在后面加了一個TodoItem實例,如果說在前面加的話又會出現(xiàn)什么問題呢?
<ul>
<TodoItem text="zero" completed={false} />
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>
和之前的代碼實例相比,此時的React會這么處理:
- 首先認為把text從first改為了zero
- second改為了first
- 最后多出了一個TodoItem實例內(nèi)容為second
現(xiàn)存(first,second)的兩個實例的屬性被改變了,強迫他們完成了一個更新! 雖然這種情況只是改變了2個組件的屬性,如果說有一百個TodoItem實例的話,明顯就是一個浪費!
后面React提供了方法來克服這種浪費,于是有了key
02|Key的用法
React中,需要確定每一個組件在組件序列中的唯一標識就是它的位置! 因此React本身也不懂哪些子組件實質(zhì)上面沒有改變! key就是每一個組件的唯一標識!
<ul>
<TodoItem key={1} text="zero" completed={false} />
<TodoItem key={2} text="first" completed={false} />
<TodoItem key={3} text="second" completed={false} />
</ul>
- key的值需要保證唯一性
- 通過key的使用配合shouldComponentUpdate就能夠一定程度上面提高性能!
03|使用reselect提高數(shù)據(jù)獲取性能
對應的除了通過優(yōu)化渲染過程來提高性能,既然React和Redux都是通過數(shù)據(jù)驅(qū)動渲染過程,除了渲染過程,獲取數(shù)據(jù)的過程也是一個需要考慮的優(yōu)化點!
通過mapStateToProps函數(shù)從Redux store提供的state中產(chǎn)生渲染需要的數(shù)據(jù),對應的代碼如下所示:
const selectVisibleTodos = (todo,filter)=>{
switch(filter){
case FilterType.ALL:
return todos;
case FilterType.COMPLETED:
return todos.filter(item=>item.completed);
case FilterTypes.UNCOMPPLETED:
return todos.filter(item=>!item.completed);
default:
throw new Error("unsupport filter!");
}
}
const mapStateToProps = state=>{
return {todos:selectVisibleTodos(state.todos,state.filter)};
}
Redux Store上獲取數(shù)據(jù)的重要一環(huán),mapStateToProps函數(shù)一定要快,從代碼來看,運算本身沒有什么課優(yōu)化的空間,
獲取對應的待辦事項,需要通過對應的todos和filter兩個字段的值計算出來! 計算過程需要遍歷todos字段上的數(shù)組,數(shù)組比較大的時候,TodoList組件的每一次重新渲染都需要重新計算一遍,負擔就會過重!
- 兩階段選擇過程
對應的selectVisibleTodos函數(shù)的計算必不可少,那么對應的如何優(yōu)化呢?
并不是每一次TodoList的渲染都需要執(zhí)行selectVisibleTodos中的計算過程,如果對應的Redux Store狀態(tài)樹上的待辦事項的todos字段沒有變化,而代表當前過濾器的filter字段也沒有變化,實在沒有必要重新渲染todos數(shù)組來計算一個新的結(jié)果! 如果說上一次的結(jié)果能夠被緩存過來的話,那么就重用緩存就行了!
reselect庫的工作原理就是,只要相關(guān)狀態(tài)沒有改變的話,那就直接重用上一次的緩存!
reselect庫被用來創(chuàng)造 選擇器:接受一個state作為參數(shù),并且通過選擇器返回的函數(shù)的數(shù)據(jù)就是我們某個mapStateToProps需要的結(jié)果! 但是選擇器不是純函數(shù),一種有記憶力的函數(shù),運行選擇器函數(shù)會有副作用!
- 通過輸入?yún)?shù)state抽取第一層結(jié)果,降低一層結(jié)果與之前的結(jié)果進行比對,如果完全相同沒必要進行比對,這一部分的比較,就是JavaScript中的全等操作符比較! 如果是對象且是同一個對象才會被認為是相同! 否則進入到下一步
- 接下來的就是確定選擇器步驟一和步驟二分別進行什么計算,原則很簡單:
- 步驟一:盡量快,運算非常簡單的,最好就是一個映射運算 通常是state參數(shù)中某個字段的引用!
- 之后的活交給第二步去計算!
對上面的代碼進行改造就需要使用 reselect
庫:
import {createSelector} from "reselect";
import {FilterTypes} from "../constants.js";
const selectVisibleTodos = createSelector([getFilter,getTodos],(filter,todos)=>{
switch(filter){
case FilterTypes.ALL:
return todos;
case FilterTypes.COMPLETED:
return todos.filter(item=>item.completed);
case FilterTypes.UNCOMPLETED:
return todos.filter(item=>!item.completed);
default:
throw new Error("unsupport filter!");
}
})
const getFilter = state=> state.filter;
const getTodos = state=> state.todos;
import {selectVisibleTodos} from "../selector.js";
const mapStateToProps = state=>{
return {todos:selectVisibleTodos(state)};
}
這樣一來雖然說createSelector接受的所有函數(shù)都是為純函數(shù),但是選擇器有記憶的副作用,只要對應的state沒有變化自然輸出也沒有變化!
只要是Redux store狀態(tài)樹上的filter和todos字段不變的話,怎么觸發(fā)TodoList的渲染過程,都不會觸發(fā)遍歷todos字段的計算,性能自然更快!
- 狀態(tài)樹的設計盡量的范式化,按照一定的設計規(guī)約he關(guān)系型數(shù)據(jù)庫的設計原則,減少數(shù)據(jù)的冗余!(數(shù)據(jù)冗余造成的后果就是難以保證數(shù)據(jù)的一致性!)