如何快速入門VUE3.0:進入學習
在本教程中,我們將學習如何在React 中實現Memoization。 Memoization 透過快取函數呼叫的結果並在再次需要時傳回這些快取的結果來提高效能。
我們將介紹以下內容:
本文假設你對React 中的類別和函數元件有基本的了解。
如果你想查閱這些主題,可以查看React 官方文件components and props
https://reactjs.org/docs/components-and-props.html
在討論React 中的Memoization 細節之前,讓我們先來看看React 是如何使用虛擬DOM 渲染UI 的。
常規DOM 基本上包含一組用樹的形式保存的節點。 DOM 中的每個節點代表一個UI 元素。每當應用程式中出現狀態變更時,該UI 元素及其所有子元素的對應節點都會在DOM 樹中更新,然後觸發UI 重繪。
在高效率的DOM 樹演算法的幫助下,更新節點的速度更快,但重繪的速度很慢,當該DOM 具有大量UI 元素時,可能會影響效能。因此,在React 中引入了虛擬DOM。
這是真實DOM 的虛擬表示。現在,每當應用程式的狀態有任何變化時,React 不會直接更新真正的DOM,而是建立一個新的虛擬DOM。然後React 會將此新的虛擬DOM 與先前創建的虛擬DOM 進行比較,找到有差異的地方(譯者註:也就是找到需要更新節點),然後進行重繪。
根據這些差異,虛擬DOM 能更有效率地更新真正的DOM。這提高了效能,因為虛擬DOM 不會簡單地更新UI 元素及其所有子元素,而是有效地僅更新實際DOM 中必要且最小的變更。
在上一節中,我們看到了React 如何使用虛擬DOM 有效地執行DOM 更新操作來提高效能。在本節中,我們將介紹一個例子,該例子解釋了為了進一步提高效能而需要使用Memoization。
我們將建立一個父類,包含一個按鈕,用於遞增名為count
的變數。父元件也呼叫了子元件,並向其傳遞參數。我們也在render
方法中加入了console.log()
語句:
//Parent.js class Parent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState((prevState) => { return { count: prevState.count + 1 }; }); }; render() { console.log("Parent render"); return ( <div className="App"> <button onClick={this.handleClick}>Increment</button> <h2>{this.state.count}</h2> <Child name={"joe"} /> </div> ); } } export default Parent;
此範例的完整程式碼可在CodeSandbox 上查看。
我們將建立一個Child
類,該類別接受父元件傳遞的參數並將其顯示在UI 中:
//Child.js class Child extends React.Component { render() { console.log("Child render"); return ( <div> <h2>{this.props.name}</h2> </div> ); } } export default Child;
每當我們點擊父元件中的按鈕時, count
值都會改變。由於state 變化了,因此父元件的render
方法被執行了。
傳遞給子元件的參數在每次父元件重新渲染時都沒有改變,因此子元件不應重新渲染。然而,當我們運行上面的程式碼並繼續遞增count
時,我們得到了以下輸出:
Parent render Child render Parent render Child render Parent render Child render
你可以在這個sandbox 體驗上述範例,並查看控制台的輸出結果。
從輸出中,我們可以看到,當父元件重新渲染時,即使傳遞給子元件的參數保持不變,子元件也會重新渲染。這將導致子元件的虛擬DOM 與先前的虛擬DOM 執行差異檢查。由於我們的子元件中沒有變更且重新渲染時的所有props 都沒變,所以真正的DOM 不會被更新。
真正的DOM 不會進行不必要地更新對效能確實是有好處,但是我們可以看到,即使子元件中沒有實際更改,也會建立新的虛擬DOM 並執行差異檢查。對於小型React 元件,這種效能消耗可以忽略不計,但對於大型元件,效能影響會很大。為了避免這種重新渲染和虛擬DOM 的差異檢查,我們使用Memoization。
在React 應用的上下文中,Memoization 是一種手段,每當父元件重新渲染時,子元件僅在它所依賴的props 發生變化時才會重新渲染。如果子元件所依賴的props 中沒有更改,則它不會執行render 方法,並將傳回快取的結果。由於渲染方法未執行,因此不會有虛擬DOM 建立和差異檢查,從而實現效能的提升。
現在,讓我們看看如何在類別和函數元件中實作Memoization,以避免這種不必要的重新渲染。
為了在類別元件中實作Memoization,我們將使用React.PureComponent。 React.PureComponent
實作了shouldComponentUpdate(),它對state
和props
進行了淺比較,並且僅在props 或state 發生更改時才重新渲染React 元件。
將子元件變更為如下所示的程式碼:
//Child.js class Child extends React.PureComponent { // 這裡我們把React.Component 改成了React.PureComponent render() { console.log("Child render"); return ( <div> <h2>{this.props.name}</h2> </div> ); } } export default Child;
此範例的完整程式碼顯示在這個sandbox 中。
父組件保持不變。現在,當我們在父元件中增加count
時,控制台中的輸出如下所示:
Parent render Child render Parent render Parent render
對於首次渲染,它同時呼叫父元件和子元件的render
方法。
對於每次增加count
後的重新渲染,僅呼叫父元件的render
函數。子元件不會重新渲染。
為了在函數元件中實作Memoization,我們將使用React.memo()。 React.memo()
是一個高階元件(HOC),它執行與PureComponent
類似的工作,來避免不必要的重新渲染。
以下是函數元件的程式碼:
//Child.js export function Child(props) { console.log("Child render"); return ( <div> <h2>{props.name}</h2> </div> ); } export default React.memo(Child); // 這裡我們為子元件新增HOC 實作Memoization
同時也將父元件轉換為了函數元件,如下所示:
//Parent.js export default function Parent() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; console.log("Parent render"); return ( <div> <button onClick={handleClick}>Increment</button> <h2>{count}</h2> <Child name={"joe"} /> </div> ); }
此範例的完整程式碼可以在這個sandbox 中看到。
現在,當我們遞增父元件中的count
時,以下內容將輸出到控制台:
Parent render Child render Parent render Parent render Parent render
在上面的範例中,我們看到,當我們對子元件使用React.memo()
HOC 時,子元件沒有重新渲染,即使父元件重新渲染了。
但是,需要注意的一個小問題是,如果我們將函數作為參數傳遞給子元件,即使使用React.memo()
之後,子元件也會重新渲染。讓我們來看一個這樣的例子。
我們將更改父元件,如下所示。在這裡,我們新增了一個處理函數,並作為參數傳遞給子元件:
//Parent.js export default function Parent() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; const handler = () => { console.log("handler"); // 這裡的handler 函式將會被傳遞給子元件}; console.log("Parent render"); return ( <div className="App"> <button onClick={handleClick}>Increment</button> <h2>{count}</h2> <Child name={"joe"} childFunc={handler} /> </div> ); }
子元件程式碼將保持原樣。我們不會在子元件中使用父元件傳遞來的函數:
//Child.js export function Child(props) { console.log("Child render"); return ( <div> <h2>{props.name}</h2> </div> ); } export default React.memo(Child);
現在,當我們遞增父元件中的count
時,它會重新渲染並同時重新渲染子元件,即使傳遞的參數中沒有更改。
那麼,是什麼原因導致子元件重新渲染的呢?答案是,每次父元件重新渲染時,都會建立一個新的handler
函數並將其傳遞給子元件。現在,由於每次重新渲染時都會重新建立handle
函數,因此子元件在對props 進行淺比較時會發現handler
引用已更改,並重新渲染子元件。
接下來,我們將介紹如何解決此問題。
useCallback()
來避免更多的重複渲染導致子元件重新渲染的主要問題是重新建立了handler
函數,這更改了傳遞給子元件的參考。因此,我們需要有一種方法來避免這種重複創建。如果未重新建立handler
函數,則對handler
函數的參考不會更改,因此子元件不會重新渲染。
為了避免每次渲染父元件時都重新建立函數,我們將使用一個名為useCallback() 的React Hook。 Hooks 是在React 16 中引入的。要了解有關Hooks 的更多信息,你可以查看React 的官方hooks 文檔,或者查看`React Hooks: How to Get Started & Build Your Own"。useCallback
useCallback()
鉤子傳入兩個參數:回調函數和依賴項列表。
useCallback()
//Do something }, [x,y]);
在這裡, useCallback()
被加入到handleClick()
函數。第二個參數[x, y]
可以是空數組、單一依賴項或相依性清單。每當第二個參數中提到的任何依賴項發生變更時,才會重新建立handleClick()
函數。
如果useCallback()
中提到的依賴項沒有更改,則傳回作為第一個參數提及的回呼函數的Memoization 版本。我們將更改父元件,以便對傳遞給子元件的處理程序使用useCallback()
鉤子:
//Parent.js export default function Parent() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; const handler = useCallback(() => { // 給handler 函數使用useCallback() console.log("handler"); }, []); console.log("Parent render"); return ( <div className="App"> <button onClick={handleClick}>Increment</button> <h2>{count}</h2> <Child name={"joe"} childFunc={handler} /> </div> ); }
子元件程式碼將保持原樣。
此範例的完整程式碼這個sandbox 中。
當我們在上述程式碼的父元件中增加count
時,我們可以看到以下輸出:
Parent render Child render Parent render Parent render Parent render
由於我們對父元件中的handler
使用了useCallback()
鉤子,因此每次父元件重新渲染時,都不會重新建立handler
函數,並且會將handler
的Memoization 版本傳遞到子元件。子元件將進行淺比較,並注意到handler
函數的參考沒有更改,因此它不會呼叫render
方法。
Memoization 是一種很好的手段,可以避免在元件的state 或props 沒有改變時對元件進行不必要的重新渲染,從而提高React 應用的效能。你可能會考慮為所有元件添加Memoization,但這並不一定是建立高效能React 元件的方法。只有在元件出現以下情況時,才應使用Memoization:
在本教程中,我們理解了:
React.memo()
和類別元件的React.PureComponent
實作MemoizationReact.memo()
之後,子元件也會重新渲染useCallback()
鉤子來避免在函數作為props 傳遞給子元件時產生重新渲染的問題希望這篇React Memoization 的介紹對你有幫助!
原文網址:https://www.sitepoint.com/implement-memoization-in-react-to-improve-performance/
原文作者:Nida Khan