react性能優化
剛開始寫react可能只是寫出來完成業務就完了,後期審查代碼發現可能很多地方其實都可以優化,之前可能有些地方似是而非,在此小結一下。
一些概念
Virtual DOM
react引入了一個叫做虛擬DOM的概念,安插在JavaScript邏輯和實際的DOM之間。這一概念提高了Web性能。在UI渲染過程中,React通過在虛擬DOM中的微操作來實對現實際DOM的局部更新。
在Web開發中,我們總需要將變化的數據實時反應到UI上,這時就需要對DOM進行操作。而復雜或頻繁的DOM操作通常是性能瓶頸產生的原因,React為此引入了虛擬DOM(Virtual DOM)的機制:在瀏覽器端用Javascript實現了一套DOM API。基於React進行開發時所有的DOM構造都是通過虛擬DOM進行,每當數據變化時,React都會重新構建整個DOM樹,然後React將當前整個DOM樹和上一次的DOM樹進行對比,得到DOM結構的區別,然後僅僅將需要變化的部分進行實際的瀏覽器DOM更新。而且React能夠批處理虛擬DOM的刷新,在一個事件循環(Event Loop)內的兩次數據變化會被合並,例如你連續的先將節點內容從A變成B,然後又從B變成A,React會認為UI不發生任何變化,而如果通過手動控制,這種邏輯通常是極其復雜的。盡管每一次都需要構造完整的虛擬DOM樹,但是因為虛擬DOM是內存數據,性能是極高的,而對實際DOM進行操作的僅僅是Diff部分,因而能達到提高性能的目的。這樣,在保證性能的同時,開發者將不再需要關註某個數據的變化如何更新到一個或多個具體的DOM元素,而只需要關心在任意一個數據狀態下,整個界面是如何Render的。
render
react的組件渲染分為初始化渲染和更新渲染。
- 初始化渲染
- 在初始化渲染的時候會調用根組件下的所有組件的render方法進行渲染
- 更新渲染
- 當我們要更新某個子組件的時候,我們期待的是只變化需要變化的組件,其他組件保持不變。
- 但是,react的默認做法是調用所有組件的render,再對生成的虛擬DOM進行對比,如不變則不進行更新。這樣的render和虛擬DOM的對比明顯是在浪費
Chrome Performance
在開發模式下, 在支持的瀏覽器內使用性能工具可以直觀的了解組件何時掛載,更新和卸載
- 打開Chrome開發工具Performance 標簽頁點擊Record
- 執行你想要分析的動作。不要記錄超過20s,不然Chrome可能會掛起。
- 停止記錄。
- React事件將會被歸類在 User Timing標簽下。
優化
bind函數
綁定this的方式:一般有下面幾種方式
- constructor中綁定
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this); //構造函數中綁定
}
//然後可以
<p onClick={this.handleClick}>
- 使用時綁定
<p onClick={this.handleClick.bind(this)}>
- 箭頭函數
<p onClick={() => { this.handleClick() }}>
- 哪個好呢
- 答案是第一種方式。
- 因為第一種,構造函數每一次渲染的時候只會執行 一遍;
- 而第二種方法,在每次render()的時候都會重新執行一遍函數;
第三種方法的話,每一次render()的時候,都會生成一個新的箭頭函數
shouldComponentUpdate
shouldComponentUpdate是決定react組件什麽時候能夠不重新渲染的函數,返回true時更新,false時不更新。默認返回true,即每次重新渲染,因此我們可以重寫個函數從而達到"個性化定制更新"的效果。
- 栗子
class Title extends React.Component {
constructor(props) {
super(props)
}
render() {
console.log(‘title render‘)
return (
<div>{this.props.title}</div>
)
}
}
class PureCom extends React.Component {
constructor(props) {
super(props)
this.state = {
title: ‘pure‘,
num: 0
}
this.add = this.add.bind(this);
}
add() {
let { num } = this.state;
num++;
this.setState({ num })
}
render() {
console.log(‘pure render‘)
return (
<div>
<Title title={this.state.title} />
<p>{this.state.num}</p>
<button onClick={this.add}>add</button>
</div>
)
}
}
- 現在每次點擊add按鈕,父組件state的num都會+1,而title是一直不變的,通過console我們卻發現,Title組件也在一直render,這就是因為shouldComponentUpdate默認返回true的,也就是父組件更新之後,子組件也會更新。
- 然後子組件是沒必要更新的,所以我們重寫下shouldComponentUpdate方法
class Title extends React.Component {
constructor(props) {
super(props)
}
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.title != this.props.title) {
return true //只有title變化時才更新
} else {
return false
}
}
render() {
console.log(‘title render‘)
return (
<div>{this.props.title}</div>
)
}
}
現在就對了,點擊父組件的add按鈕並沒有觸發Title組件的更新。
PureComponent
類似上面的情況其實我們經常遇到,因此react提供了PureComponent來解決類似的問題,可以讓我們少寫許多的shouldComponentUpdate。
class Title extends React.PureComponent {
constructor(props) {
super(props)
}
render() {
console.log(‘title render‘)
return (
<div>{this.props.title}</div>
)
}
}
- 用了PureComponent之後作用和之前是相同的。
- 原理:當組件更新時,如果組件的 props 和 state 都沒發生改變, render 方法就不會觸發,省去 Virtual DOM 的生成和比對過程,達到提升性能的目的。具體就是 React 自動幫我們做了一層淺比較:
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps)
|| !shallowEqual(inst.state, nextState);
}
突變的數據
大多數情況PureComponent都可以解決,但是之前也說過,他是“淺比較”,如果遇到數據結構比較復雜,他是無法識別的。
class PureCom extends PureComponent {
constructor(props) {
super(props)
this.state = {
items: [1, 2, 3],
title: ‘pure‘,
}
this.add = this.add.bind(this);
}
add() {
let { items } = this.state;
items.push(23);
this.setState({ items })
}
render() {
console.log(‘pure render‘)
return (
<div>
<Title title={this.state.title} />
<ul>
{this.state.items.map((e, i) => {
return <li key={i}>{e}</li>
})}
</ul>
<button onClick={this.add}>add</button>
</div>
)
}
}
- 點擊add,你會發現沒有任何反應,為什麽呢?因為你setState的
items
其實是和state裏面的items
指向相同引用。原理和下面一樣。
let a={val:1};
let b=a;
b.val=2;
console.log(a)//{val:2}
console.log(b)//{val:2}
- 解決辦法
- 1.深拷貝
add() { let items =JSON.parse(JSON.stringify(this.state.items));//黑科技 //或者let items=deepCopy(this.state.items); items.push(23); this.setState({ items }) }
- 2.數組使用concat,對象使用Object.assign()
add() { let { items } = this.state; items=items.concat(23) //此時的items是一個新數組 this.setState({ items }) }
- 3.使用不可變數據Immutable.js
add() { let { items } = this.state; items = update(items, { $push: [23] }); this.setState({ items }) }
- 其中深拷貝如果數據比較復雜消耗會比較大
- concat,Object.assign用起來很快捷
如果你數據比較復雜,可能Immutable會是最好的選擇。官方推薦::seamless-immutable 和immutability-helper。
redux
個人感覺redux的渲染機制也是和PureComponent類似的,都是淺比較,因此上面的3種解決辦法也適用於redux.
16.3+ new API
一些生命周期會被刪除,將在17.0:刪除componentWillMount,componentWillReceiveProps和componentWillUpdate。
- 一些變化
componentWillMount
=>componentDidMount
componentWillReceiveProps
=>getDerivedStateFromProps
componentWillUpdate
=>getSnapshotBeforeUpdate
- static getDerivedStateFromProps
//代替componentWillReceiveProps,因為是靜態方法,不能訪問到 this,避免了一些可能有副作用的邏輯,比如訪問 DOM 等等
//會在第一次掛載和重繪的時候都會調用到,因此你基本不用在constructor裏根據傳入的props來setState
static getDerivedStateFromProps(nextProps, prevState) {
console.log(nextProps, prevState)
if (prevState.music !== nextProps.music) {
return {
music: nextProps.music,
music_file: music_file,
index:prevState.index+1
};
//document.getElementById(‘PLAYER‘).load(); //這裏不對,應該放在getSnapshotBeforeUpdate 和 componentDidUpdate
}
return null;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.music != prevState.music) { //進行aduio的重載
return true
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
document.getElementById(‘PLAYER‘).load(); //重載
}
}
- getSnapshotBeforeUpdate
//新的getSnapshotBeforeUpdate生命周期在更新之前被調用(例如,在DOM被更新之前)。此生命周期的返回值將作為第三個參數傳遞給componentDidUpdate。 (這個生命周期不是經常需要的,但可以用於在恢復期間手動保存滾動位置的情況。)
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) { //snapshot
// If we have a snapshot value, we‘ve just added new items.
// Adjust scroll so these new items don‘t push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
- 使用componentDidMount 代替 componentWillMount
//有一個常見的錯誤觀念認為,在componentWillMount中提取可以避免第一個空的渲染。在實踐中,這從來都不是真的,因為React總是在componentWillMount之後立即執行渲染。如果數據在componentWillMount觸發的時間內不可用,則無論你在哪裏提取數據,第一個渲染仍將顯示加載狀態。
// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._asyncRequest = asyncLoadData().then(
externalData => {
this._asyncRequest = null;
this.setState({ externalData });
}
);
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
}
其他
- props盡量只傳需要的數據,避免多余的更新
- 組件盡量解耦,比如一個input+list組建,可以將list分成一個PureComponent,只在list數據變化是更新
- 如果組件有復用,key值非常重要。因此key的優化,如果有唯一id,盡量不使用循環得到的index
- 暫時這些
最後
大家好,這裏是「 TaoLand 」,這個博客主要用於記錄一個菜鳥程序猿的Growth之路。這也是自己第一次做博客,希望和大家多多交流,一起成長!文章將會在下列地址同步更新……
個人博客:www.yangyuetao.cn
小程序:TaoLand
react性能優化