文章首發我的博客javascript
本文只介紹函數式組件特有的性能優化方式,類組件和函數式組件都有的不介紹,好比 key 的使用。另外本文不詳細的介紹 API 的使用,後面也許會寫,其實想用好 hooks 仍是蠻難的。php
有過 React 函數式組件的實踐,而且對 hooks 有過實踐,對 useState、useCallback、useMemo API 至少看過文檔,若是你有過對類組件的性能優化經歷,那麼這篇文章會讓你有種熟悉的感受。html
我以爲React 性能優化的理念的主要方向就是這兩個:前端
減小從新 render 的次數。由於在 React 裏最重(花時間最長)的一塊就是 reconction(簡單的能夠理解爲 diff),若是不 render,就不會 reconction。vue
減小計算的量。主要是減小重複計算,對於函數式組件來講,每次 render 都會從新從頭開始執行函數調用。java
在使用類組件的時候,使用的 React 優化 API 主要是:shouldComponentUpdate
和 PureComponent
,這兩個 API 所提供的解決思路都是爲了減小從新 render 的次數,主要是減小父組件更新而子組件也更新的狀況,雖然也能夠在 state 更新的時候阻止當前組件渲染,若是要這麼作的話,證實你這個屬性不適合做爲 state,而應該做爲靜態屬性或者放在 class 外面做爲一個簡單的變量 。react
可是在函數式組件裏面沒有聲明週期也沒有類,那如何來作性能優化呢?api
首先要介紹的就是 React.memo
,這個 API 能夠說是對標類組件裏面的 PureComponent
,這是能夠減小從新 render 的次數的。數組
舉個🌰,首先咱們看兩段代碼:緩存
在根目錄有一個 index.js,代碼以下,實現的東西大概就是:上面一個 title,中間一個 button(點擊 button 修改 title),下面一個木偶組件,傳遞一個 name 進去。
// index.js import React, { useState } from "react"; import ReactDOM from "react-dom"; import Child from './child' function App() { const [title, setTitle] = useState("這是一個 title") return ( <div className="App"> <h1>{ title }</h1> <button onClick={() => setTitle("title 已經改變")}>更名字</button> <Child name="桃桃"></Child> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
在同級目錄有一個 child.js
// child.js import React from "react"; function Child(props) { console.log(props.name) return <h1>{props.name}</h1> } export default Child
當首次渲染的時候的效果以下:
而且控制檯會打印"桃桃」
,證實 Child 組件渲染了。
接下來點擊更名字這個 button,頁面會變成:
title 已經改變了,並且控制檯也打印出"桃桃"
,能夠看到雖然咱們改的是父組件的狀態,父組件從新渲染了,而且子組件也從新渲染了。你可能會想,傳遞給 Child 組件的 props 沒有變,要是 Child 組件不從新渲染就行了,爲何會這麼想呢?
咱們假設 Child 組件是一個很是大的組件,渲染一次會消耗不少的性能,那麼咱們就應該儘可能減小這個組件的渲染,不然就容易產生性能問題,因此子組件若是在 props 沒有變化的狀況下,就算父組件從新渲染了,子組件也不該該渲染。
那麼咱們怎麼才能作到在 props 沒有變化的時候,子組件不渲染呢?
答案就是用 React.memo
在給定相同 props 的狀況下渲染相同的結果,而且經過記憶組件渲染結果的方式來提升組件的性能表現。
把聲明的組件經過React.memo
包一層就行了,React.memo
實際上是一個高階函數,傳遞一個組件進去,返回一個能夠記憶的組件。
function Component(props) { /* 使用 props 渲染 */ } const MyComponent = React.memo(Component);
那麼上面例子的 Child 組件就能夠改爲這樣:
import React from "react"; function Child(props) { console.log(props.name) return <h1>{props.name}</h1> } export default React.memo(Child)
經過 React.memo
包裹的組件在 props 不變的狀況下,這個被包裹的組件是不會從新渲染的,也就是說上面那個例子,在我點擊更名字以後,僅僅是 title 會變,可是 Child 組件不會從新渲染(表現出來的效果就是 Child 裏面的 log 不會在控制檯打印出來),會直接複用最近一次渲染的結果。
這個效果基本跟類組件裏面的 PureComponent
效果極其相似,只是前者用於函數組件,後者用於類組件。
默認狀況下其只會對 props 的複雜對象作淺層對比(淺層對比就是隻會對比先後兩次 props 對象引用是否相同,不會對比對象裏面的內容是否相同),若是你想要控制對比過程,那麼請將自定義的比較函數經過第二個參數傳入來實現。
function MyComponent(props) { /* 使用 props 渲染 */ } function areEqual(prevProps, nextProps) { /* 若是把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 不然返回 false */ } export default React.memo(MyComponent, areEqual);
此部分來自於 React 官網。
若是你有在類組件裏面使用過 shouldComponentUpdate()
這個方法,你會對 React.memo
的第二個參數很是的熟悉,不過值得注意的是,若是 props 相等,areEqual
會返回 true
;若是 props 不相等,則返回 false
。這與 shouldComponentUpdate
方法的返回值相反。
如今根據上面的例子,再改一下需求,在上面的需求上增長一個副標題,而且有一個修改副標題的 button,而後把修改標題的 button 放到 Child 組件裏。
把修改標題的 button 放到 Child 組件的目的是,將修改 title 的事件經過 props 傳遞給 Child 組件,而後觀察這個事件可能會引發性能問題。
首先看代碼:
父組件 index.js
// index.js import React, { useState } from "react"; import ReactDOM from "react-dom"; import Child from "./child"; function App() { const [title, setTitle] = useState("這是一個 title"); const [subtitle, setSubtitle] = useState("我是一個副標題"); const callback = () => { setTitle("標題改變了"); }; return ( <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副標題改變了")}>改副標題</button> <Child onClick={callback} name="桃桃" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
子組件 child.js
import React from "react"; function Child(props) { console.log(props); return ( <> <button onClick={props.onClick}>改標題</button> <h1>{props.name}</h1> </> ); } export default React.memo(Child);
首次渲染的效果
這段代碼在首次渲染的時候會顯示上圖的樣子,而且控制檯會打印出桃桃
。
而後當我點擊改副標題這個 button 以後,副標題會變爲「副標題改變了」,而且控制檯會再次打印出桃桃
,這就證實了子組件又從新渲染了,可是子組件沒有任何變化,那麼此次 Child 組件的從新渲染就是多餘的,那麼如何避免掉這個多餘的渲染呢?
咱們在解決問題的以前,首先要知道這個問題是什麼緣由致使的?
我們來分析,一個組件從新從新渲染,通常三種狀況:
要麼是組件本身的狀態改變
要麼是父組件從新渲染,致使子組件從新渲染,可是父組件的 props 沒有改版
要麼是父組件從新渲染,致使子組件從新渲染,可是父組件傳遞的 props 改變
接下來用排除法查出是什麼緣由致使的:
第一種很明顯就排除了,當點擊改副標題 的時候並無去改變 Child 組件的狀態;
第二種狀況好好想一下,是否是就是在介紹 React.memo
的時候狀況,父組件從新渲染了,父組件傳遞給子組件的 props 沒有改變,可是子組件從新渲染了,咱們這個時候用 React.memo
來解決了這個問題,因此這種狀況也排除。
那麼就是第三種狀況了,當父組件從新渲染的時候,傳遞給子組件的 props 發生了改變,再看傳遞給 Child 組件的就兩個屬性,一個是 name
,一個是 onClick
,name
是傳遞的常量,不會變,變的就是 onClick
了,爲何傳遞給 onClick 的 callback 函數會發生改變呢?在文章的開頭就已經說過了,在函數式組件裏每次從新渲染,函數組件都會重頭開始從新執行,那麼這兩次建立的 callback 函數確定發生了改變,因此致使了子組件從新渲染。
找到問題的緣由了,那麼解決辦法就是在函數沒有改變的時候,從新渲染的時候保持兩個函數的引用一致,這個時候就要用到 useCallback
這個 API 了。
const callback = () => { doSomething(a, b); } const memoizedCallback = useCallback(callback, [a, b])
把函數以及依賴項做爲參數傳入 useCallback
,它將返回該回調函數的 memoized 版本,這個 memoizedCallback 只有在依賴項有變化的時候纔會更新。
那麼能夠將 index.js 修改成這樣:
// index.js import React, { useState, useCallback } from "react"; import ReactDOM from "react-dom"; import Child from "./child"; function App() { const [title, setTitle] = useState("這是一個 title"); const [subtitle, setSubtitle] = useState("我是一個副標題"); const callback = () => { setTitle("標題改變了"); }; // 經過 useCallback 進行記憶 callback,並將記憶的 callback 傳遞給 Child const memoizedCallback = useCallback(callback, []) return ( <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副標題改變了")}>改副標題</button> <Child onClick={memoizedCallback} name="桃桃" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
這樣咱們就能夠看到只會在首次渲染的時候打印出桃桃,當點擊改副標題和改標題的時候是不會打印桃桃的。
若是咱們的 callback 傳遞了參數,當參數變化的時候須要讓它從新添加一個緩存,能夠將參數放在 useCallback 第二個參數的數組中,做爲依賴的形式,使用方式跟 useEffect 相似。
在文章的開頭就已經介紹了,React 的性能優化方向主要是兩個:一個是減小從新 render 的次數(或者說減小沒必要要的渲染),另外一個是減小計算的量。
前面介紹的 React.memo 和 useCallback 都是爲了減小從新 render 的次數。對於如何減小計算的量,就是 useMemo 來作的,接下來咱們看例子。
function App() { const [num, setNum] = useState(0); // 一個很是耗時的一個計算函數 // result 最後返回的值是 49995000 function expensiveFn() { let result = 0; for (let i = 0; i < 10000; i++) { result += i; } console.log(result) // 49995000 return result; } const base = expensiveFn(); return ( <div className="App"> <h1>count:{num}</h1> <button onClick={() => setNum(num + base)}>+1</button> </div> ); }
首次渲染的效果以下:
這個例子功能很簡單,就是點擊 +1 按鈕,而後會將如今的值(num) 與 計算函數 (expensiveFn) 調用後的值相加,而後將和設置給 num 並顯示出來,在控制檯會輸出 49995000
。
就算是一個看起來很簡單的組件,也有可能產生性能問題,經過這個最簡單的例子來看看還有什麼值得優化的地方。
首先咱們把 expensiveFn 函數當作一個計算量很大的函數(好比你能夠把 i 換成 10000000),而後當咱們每次點擊 +1 按鈕的時候,都會從新渲染組件,並且都會調用 expensiveFn 函數並輸出 49995000
。因爲每次調用 expensiveFn 所返回的值都同樣,因此咱們能夠想辦法將計算出來的值緩存起來,每次調用函數直接返回緩存的值,這樣就能夠作一些性能優化。
針對上面產生的問題,就能夠用 useMemo 來緩存 expensiveFn 函數執行後的值。
首先介紹一下 useMemo 的基本的使用方法,詳細的使用方法可見官網:
function computeExpensiveValue() { // 計算量很大的代碼 return xxx } const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的第一個參數就是一個函數,這個函數返回的值會被緩存起來,同時這個值會做爲 useMemo 的返回值,第二個參數是一個數組依賴,若是數組裏面的值有變化,那麼就會從新去執行第一個參數裏面的函數,並將函數返回的值緩存起來並做爲 useMemo 的返回值 。
瞭解了 useMemo 的使用方法,而後就能夠對上面的例子進行優化,優化代碼以下:
function App() { const [num, setNum] = useState(0); function expensiveFn() { let result = 0; for (let i = 0; i < 10000; i++) { result += i; } console.log(result) return result; } const base = useMemo(expensiveFn, []); return ( <div className="App"> <h1>count:{num}</h1> <button onClick={() => setNum(num + base)}>+1</button> </div> ); }
執行上面的代碼,而後如今能夠觀察不管咱們點擊 +1多少次,只會輸出一次 49995000,這就表明 expensiveFn 只執行了一次,達到了咱們想要的效果。
useMemo 的使用場景主要是用來緩存計算量比較大的函數結果,能夠避免沒必要要的重複計算,有過 vue 的使用經歷同窗可能會以爲跟 Vue 裏面的計算屬性有殊途同歸的做用。
不過另外提醒兩點
1、若是沒有提供依賴項數組,
useMemo
在每次渲染時都會計算新的值;2、計算量若是很小的計算函數,也能夠選擇不使用 useMemo,由於這點優化並不會做爲性能瓶頸的要點,反而可能使用錯誤還會引發一些性能問題。
對於性能瓶頸可能對於小項目遇到的比較少,畢竟計算量小、業務邏輯也不復雜,可是對於大項目,極可能是會遇到性能瓶頸的,可是對於性能優化有不少方面:網絡、關鍵路徑渲染、打包、圖片、緩存等等方面,具體應該去優化哪方面還得本身去排查,本文只介紹了性能優化中的冰山一角:運行過程當中 React 的優化。
合理拆分組件還有不少其餘好處,好比好維護,並且這是學習組件化思想的第一步,合理的拆分組件又是一門藝術了,若是拆分得不合理,就有可能致使狀態混亂,多敲代碼多思考。
我這裏只介紹了函數式組件的優化方式,更多的 React 優化技巧能夠閱讀下面的文章:
我是桃翁,一個愛思考的前端er,想了解關於更多的前端相關的,請關注個人公號:「前端桃園」,若是想加入交流羣關注公衆號後回覆「微信」拉你進羣
本文由博客一文多發平臺 OpenWrite 發佈!