1. 程式人生 > >快取 React 事件監聽器來提高效能

快取 React 事件監聽器來提高效能

在 js 裡面有個不被重視的概念:物件和函式的引用,而這個卻直接地影響了 React 的效能。如果你打算建立兩個相同的函式,但是卻又不相等。你可以試著:

JavaScript
123 constfunctionOne=function(){alert('Hello world!');};constfunctionTwo=function(){alert('Hello world!');};functionOne===functionTwo;// false

如果將一個變數指向一個已經存在的函式,看看它們的不同:

JavaScript
123 constfunctionThree=function(){alert('Hello world!');};constfunctionFour=functionThree;functionThree===functionFour;// true

物件也是這樣的。

JavaScript
12345 constobject1={};constobject2={};constobject3=object1;object1===object2;// falseobject1===object3;// true

如果你學過其他語言,可能會熟悉指標。每次你建立物件的時候,你都會為其分配裝置記憶體。當宣告 oject1 = {} 的時候,將會在使用者的 RAM 中建立一串位元組給到 object1。可想而知,object1 就是一個儲存了鍵值對存放在 RAM 的地址。而宣告 object2 = {},將會在 RAM 中建立另外一串不同的位元組給到object2object上地址和object2 的一樣嗎?不是的。這也為什麼這兩個變數的是不相等。他們的鍵值對可能會完全相同,但是他們在記憶體中的地址是不一樣的,這才是會被比較的地方。

若使得object3 = object1,會讓object3的值為object1的地址。這不是一個新的物件。記憶體中的位置是一樣的。可以如下驗證:

JavaScript
1234 constobject1={x:true};constobject3=object1;object3.x=false;object1.x;// false

這個例子裡,在記憶體中建立物件並指向object1。讓後讓object3等於同樣的記憶體地址。通過修改object3,可以改變對應記憶體中的值,這也意味著所有指向該記憶體的變數都會被修改。obect1,仍指向該記憶體,所以值也被改變了。

初級工程師會犯這種非常常見的錯誤,並且需要深入學習相關教程;只是本文是討論 React 效能的,甚至是對變數引用有較深資歷的開發者也可能需要學習。

這個和 React 有什麼關係呢?React 有個節省執行時間的聰明方式,可以優化效能:如果元件的 props 和 state 都沒有變化,render 的輸出必然也是沒有變化的。很清晰的,如果所有的都一樣,那就意味著沒有變化。如果沒有變化,render 必須返回相同的輸出,就不用執行了。這使得 React 更加快速,按需渲染。

React 採用和 JavaScript 一樣的方式,通過簡單的 == 操作符來判斷 props 和 state 是否有變化。React 不會深入比較物件是否相等。深對比是對比物件的每一個鍵值對,而不是對比記憶體地址。React 處理方式就是淺對比,僅僅是對比一下引用是否相同而已。

如若將元件的 prop 從 { x: 1 } 改為另外一個 { x: 1 },React 將會重新渲染,因為這兩個物件在記憶體上有不用的引用。如果只是將元件的 prop 從上文中的 object1 改為 object3 ,React 是不會重新渲染的,應為這兩個物件是同一個引用。

在 Javascript,函式也是同樣的處理方式。如果 React 接收到不同記憶體地址而功能相同的函式,React 也會重新渲染。如果 React 接收到相同函式的引用,就會不重新渲染。

在程式碼稽核的時候,我就遇到下面這種常見誤用的場景:

JavaScript
123456789101112131415161718 classSomeComponent extendsReact.PureComponent{get instructions(){if(this.props.do){return'Click the button: ';}return'Do NOT click the button: ';}render(){return(<div>{this.instructions}<ButtononClick={()=>alert('!')}/></div>);}}

這是非常直接的一個元件。當按鈕被點選的時候,就 alert。instructions 用來表示是否點選了按鈕。而 SomeComponent 的 prop 的 do={true}do={false} 決定了 instructions。

這裡有問題的是,當 SomeComponent 重新渲染的時候(例如 do 屬性從 true 切換到 false),Button 也會重新渲染!儘管每次這個onClick方法都是相同的,但是每次渲染都會被重新建立。每次渲染都會在記憶體中建立新的函式(因為會在 render 函式裡重新建立),一個指向新記憶體地址的引用被傳遞到 <Button />,雖然輸入完全沒有變化,該 Button 元件還是會重新渲染。

修改

如果函式不依賴於元件(不是 this 上下文),你可以在元件的外部定義它。所有的元件例項都會用到相同的引用,因為都是同一個函式。

JavaScript
1234567891011121314151617181920 constcreateAlertBox=()=>alert('!');classSomeComponent extendsReact.PureComponent{get instructions(){if(this.props.do){return'Click the button: ';}return'Do NOT click the button: ';}render(){return(<div>{this.instructions}<ButtononClick={createAlertBox}/></div>);}}

和前面的例子相反,createAlertBox 在每次渲染中仍然有著有相同的引用。因此按鈕就不會重新渲染了。

Button 就像一個又小又快速渲染的元件,你可能在大型、複雜、渲染速度慢的元件裡面看到這些行內的定義,在 React 應用裡面真的會有很多很多。最好不要在渲染方法裡面定義這些函式。

如果函式確實依賴於元件,使得你不能在元件外部定義,你可以將元件的方法作為事件處理傳遞過去。

JavaScript
12345678910111213141516171819202122 classSomeComponent extendsReact.PureComponent{createAlertBox=()=>{alert(this.props.message);};get instructions(){if(this.props.do){return'Click the button: ';}return'Do NOT click the button: ';}render(){return(<div>{this.instructions}<ButtononClick={this.createAlertBox}/></div>);}}

在本例中,每個SomeComponent的例項有不同的告警方式。按鈕的點選事件處理需要獨立於SomeComponent。通過傳遞createAlertBox方法,他就和SomeComponent是否渲染無關了。甚至和message這個屬性是否修改也沒有關係。createAlertBox 的記憶體地址沒有改變,意味著Button沒有重新渲染。這可以節省執行時間並提升應用的渲染速度。

但是如果函式是動態的怎麼辦呢?

修改(高階)

這裡有個非常常見的使用情況,在簡單的元件裡面,有很多獨立的動態事件監聽器,例如在遍歷陣列的時候:

JavaScript
12345678910111213 classSomeComponent extendsReact.PureComponent{render(){return(<ul>{this.props.list.map(listItem=><li key={listItem.text}><ButtononClick={()=>alert(listItem.text)}/></li>)}</ul>);}}

在這個例子裡面,有不確定數量的按鈕和監聽器,每個按鈕都有獨立的函式,並且無法在元件SomComponent建立之前知道。要如何解決這個難題呢?

輸入記憶,或者更簡單的稱之為快取。對於每個唯一的值,建立和快取對應的函式。對以後這個唯一值的所有引用,都返回之前的快取函式。

這就是我如何實現上面的例子:

JavaScript
1234567891011121314151617181920212223242526272829 classSomeComponent extendsReact.PureComponent{// Each instance of SomeComponent has a cache of click handlers// that are unique to it.clickHandlers={};// Generate and/or return a click handler,// given a unique identifier.getClickHandler(key){// If no click handler exists for this unique identifier, create one.if(!Object.prototype.hasOwnProperty.call(this.clickHandlers,key)){this.clickHandlers[key]=()=>alert(key);}returnthis.clickHandlers[key];}render(){return(<ul>{this.props.list.map(listItem=><li key={listItem.text}><ButtononClick={this.getClickHandler(listItem.text)}/></li>)}</ul>);}}

陣列中的每一項都會被傳入 getClickHandler 方法中。這個方法裡面,第一次傳值呼叫的時候,會對應這個唯一的值建立函式,並返回。以後通過這個值呼叫這個方法的時候,將會不會返回新的函式,相反會返回之前在記憶體裡建立的函式的引用。

最終,重新渲染SomeComponent元件時,不會引起Button元件的重新渲染。相似的,在list裡面新增項也會為按鈕動態地建立事件監聽器。

可能需要費點腦子為事件處理函式建立唯一的標識,來區分不同的函式,但是在遍歷裡面,沒有比每個 JSX 物件生成的 key 更簡單得了。

這裡對使用 index 當作唯一標識有個提醒:如果陣列順序改了或者有刪除項,可能會獲得錯誤的返回。當將陣列從 [ 'soda', 'pizza' ] 改為 [ 'pizza' ],同時已經快取了事件監聽器為listeners[0] = () => alert('soda'),但點選 index 為 0 的按鈕 pizza 的時候,它將會彈出 soda。這也是React建議不要使用陣列的索引作為 key 的原因。

結論

如果你喜歡本文,隨意給它掌聲吧。它很快,很容易,而且免費。如果你有其他的問題或則相關的建議,請在下面留言。