快取 React 事件監聽器來提高效能
在 js 裡面有個不被重視的概念:物件和函式的引用,而這個卻直接地影響了 React 的效能。如果你打算建立兩個相同的函式,但是卻又不相等。你可以試著: const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false
如果將一個變數指向一個已經存在的函式,看看它們的不同: const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true
物件也是這樣的。 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true
如果你學過其他語言,可能會熟悉指標。每次你建立物件的時候,你都會為其分配裝置記憶體。當宣告 oject1 = {}
的時候,將會在使用者的 RAM 中建立一串位元組給到 object1
。可想而知, object1
就是一個儲存了鍵值對存放在 RAM 的地址。而宣告 object2 = {}
,將會在 RAM 中建立另外一串不同的位元組給到 object2
。 object
上地址和 object2
的一樣嗎?不是的。這也為什麼這兩個變數的是不相等。他們的鍵值對可能會完全相同,但是他們在記憶體中的地址是不一樣的,這才是會被比較的地方。
若使得 object3 = object1
,會讓 object3
的值為 object1
的地址。這不是一個新的物件。記憶體中的位置是一樣的。可以如下驗證: const object1 = { x: true }; const object3 = 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 接收到相同函式的引用,就會不重新渲染。
在程式碼稽核的時候,我就遇到下面這種常見誤用的場景 class SomeComponent extends React.PureComponent { get instructions() { if (this.props.do) { return 'Click the button: '; } return 'Do NOT click the button: '; } render() { return ( <div> {this.instructions} <Button onClick={() => alert('!')} /> </div> ); } }
這是非常直接的一個元件。當按鈕被點選的時候,就 alert。instructions 用來表示是否點選了按鈕。而 SomeComponent 的 prop 的 do={true}
或 do={false}
決定了 instructions。
這裡有問題的是,當 SomeComponent
重新渲染的時候(例如 do 屬性從 true 切換到 false), Button
也會重新渲染!儘管每次這個 onClick
方法都是相同的,但是每次渲染都會被重新建立。每次渲染都會在記憶體中建立新的函式(因為會在 render 函式裡重新建立),一個指向新記憶體地址的引用被傳遞到 <Button />
,雖然輸入完全沒有變化,該 Button
元件還是會重新渲染。
修改
如果函式不依賴於元件(不是 this
上下文),你可以在元件的外部定義它。所有的元件例項都會用到相同的引用,因為都是同一個函式。 const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() { if (this.props.do) { return 'Click the button: '; } return 'Do NOT click the button: '; } render() { return ( <div> {this.instructions} <Button onClick={createAlertBox} /> </div> ); } }
和前面的例子相反, createAlertBox
在每次渲染中仍然有著有相同的引用。因此按鈕就不會重新渲染了。
Button
就像一個又小又快速渲染的元件,你可能在大型、複雜、渲染速度慢的元件裡面看到這些行內的定義,在 React 應用裡面真的會有很多很多。最好不要在渲染方法裡面定義這些函式。
如果函式確實依賴於元件,使得你不能在元件外部定義,你可以將元件的方法作為事件處理傳遞過去。 class SomeComponent extends React.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} <Button onClick={this.createAlertBox} /> </div> ); } }
在本例中,每個 SomeComponent
的例項有不同的告警方式。按鈕的點選事件處理需要獨立於 SomeComponent
。通過傳遞 createAlertBox
方法,他就和 SomeComponent
是否渲染無關了。甚至和 message
這個屬性是否修改也沒有關係。 createAlertBox
的記憶體地址沒有改變,意味著 Button
沒有重新渲染。這可以節省執行時間並提升應用的渲染速度。
但是如果函式是動態的怎麼辦呢?
修改(高階)
這裡有個非常常見的使用情況,在簡單的元件裡面,有很多獨立的動態事件監聽器,例如在遍歷陣列的時候: class SomeComponent extends React.PureComponent { render() { return ( <ul> {this.props.list.map(listItem => <li> <Button onClick={() => alert(listItem.text)} /> </li> )} </ul> ); } }
在這個例子裡面,有不確定數量的按鈕和監聽器,每個按鈕都有獨立的函式,並且無法在元件 SomComponent
建立之前知道。要如何解決這個難題呢?
輸入記憶,或者更簡單的稱之為快取。對於每個唯一的值,建立和快取對應的函式。對以後這個唯一值的所有引用,都返回之前的快取函式。
這就是我如何實現上面的例子: class SomeComponent extends React.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); } return this.clickHandlers[key]; } render() { return ( <ul> {this.props.list.map(listItem => <li> <Button onClick={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 的原因。
結論
如果你喜歡本文,隨意給它掌聲吧。它很快,很容易,而且免費。如果你有其他的問題或則相關的建議,請在下面留言。
閱讀更多我的專欄,可以在 LinkedIn 和 Twitter , 或 check out my portfolio on CharlesStover.com 上關注我.