react Hook 之 memo,useCallback,useMemo 性能優化

前言

本文示例javascript

基礎示例

需求: 編寫個父子組件java

父組件react

import React, { useState } from 'react'
import Child from './child'
export default function Parent(props: any) {
	const [num, setNum] = useState(0)
	const handleClick = () => {
		setNum(num + 1)
	}
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} <Child /> </div>
	)
}
複製代碼

子組件git

export default function Child(props: any) {
	console.info('子組件渲染了')
	return <div>我是子組件</div>
}

複製代碼

現象:github

每次點擊"更改num"按鈕,控制檯都彈出"子組件渲染了" 子組件渲染web

React.memo

上面子組件裏並無依賴父組件任何屬性,卻在每次父組件更改時,都會被從新渲染,此時能夠用 React.memo 優化。api

定義

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});
複製代碼

React.memo 至關於 PureComponent,是個高階組件,默認對 props 作一次淺比較,若是 props 沒有更改,則子組件不會從新執行。數組

若是想要自定義對比過程,能夠經過第二個參數來實現:markdown

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /* 若是把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 不然返回 false */
}
export default React.memo(MyComponent, areEqual);
複製代碼

優化

import React, { useState } from 'react'
import Child from './child'
const MemoChild = React.memo(Child) // 新增代碼
export default function Parent(props: any) {
	const [num, setNum] = useState(0)
	const handleClick = () => {
		setNum(num + 1)
	}
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} {/* <Child /> */} {/* 新增代碼 */} <MemoChild /> </div>
	)
}

複製代碼

現象: 每次點擊"更改num"按鈕,控制檯都再也不彈出"子組件渲染了" memo 那麼這個示例是否完美了呢?若是說父子組件之間不通信,那麼這樣就可行了。但父組件要傳數據跟事件給子組件時,就仍然會有問題。app

父組件傳遞事件給子組件示例

更改父組件代碼爲:

import React, { useState } from 'react'
import Child from './child'
const MemoChild = React.memo(Child)
export default function Parent(props: any) {
	const [num, setNum] = useState<number>(0)
	const handleClick = () => {
		setNum(num + 1)
	}
	const handleChange = () => {
		console.info(2323)
	}
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} {/* 新增代碼 */} {/* 父組件傳遞 handleChange 事件給子組件 */} <MemoChild handleChange={handleChange}/> </div>
	)
}
複製代碼

現象:

每次點擊"更改num"按鈕,控制檯都彈出"子組件渲染了" 父子通信 當傳遞事件給子組件時,點擊更改 num ,控制檯會再次打印出"子組件渲染了",說明子組件又從新渲染了。

分析緣由:

首先一個組件從新渲染,通常是三種狀況致使的:

  1. 組件本身的狀態改變了
  2. 父組件從新渲染,但父組件的 props 沒有改變
  3. 父組件從新渲染,父組件傳遞的 props 改變

很明顯,第一種不是,由於子組件並無任何狀態。

第二種,已經用 React.memo 優化掉了,因此也不是。

那說明就是第三種, 傳遞的 props 改了

那爲何傳遞的 handleChange 函數會發生改變呢?

由於在函數式組件裏每次從新渲染,都會從新從頭開始執行函數調用,那麼這兩次建立的 handleChange 函數確定發生了改變,因此致使子組件從新渲染。

流程:

  1. 用戶點擊"更改num"按鈕
  2. 父組件修改 num 的狀態值
  3. 狀態值改變致使父組件從新渲染
  4. handleChange 函數體會從新註冊一遍
  5. 傳遞給 handleChange props 也就變了
  6. 子組件從新渲染

useCallback

找到緣由,那麼就要想法子,在函數沒有改變的狀況下,從新渲染的時候保持函數的引用一致

定義

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製代碼

返回一個 memoized 回調函數。

把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。

useCallback(fn, deps) 至關於 useMemo(() => fn, deps)。

優化

// 新增引入 useCallback
import React, { useState, useCallback } from 'react'
import Child from './child'
const MemoChild = React.memo(Child) 
export default function Parent(props: any) {
	const [num, setNum] = useState<number>(0)
	const handleClick = () => {
		setNum(num + 1)
	}
  // 修改代碼
  // 經過 useCallback 進行記憶 handleChange, 並將記憶的 handleChange 傳遞給 MemoChild
	const memoizedCallback = useCallback(() => {
		console.info(2323)
	},[])
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} <MemoChild handleChange={memoizedCallback} /> </div>
	)
}

複製代碼

現象:

每次點擊"更改num"按鈕,控制檯都再也不彈出"子組件渲染了" useCallback 能夠看到子組件並不會從新渲染,那麼是否是能夠結束了呢?

不,父子組件之間除了傳遞事件,還會傳遞狀態

父組件傳遞狀態給子組件示例

import React, { useState } from 'react'
import Child from './child'
const MemoChild = React.memo(Child) // 新增代碼
export default function Parent(props: any) {
	const [num, setNum] = useState<number>(0)
	const [name] = useState<string>()
	const handleClick = () => {
		setNum(num + 1)
	}
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} {/* 新增代碼 */} {/* 父組件傳遞 name 狀態給子組件 */} <MemoChild name={name} /> </div>
	)
}
複製代碼

現象:

每次點擊"更改num"按鈕,控制檯都不會彈出"子組件渲染了"。 memo 這不是正常的嗎?這不是在逗我嗎?不是的,請繼續看下來

上面傳遞的 name 是基本類型,若是是對象呢? 更改代碼以下:

import React, { useState } from 'react'
import Child from './child'
const MemoChild = React.memo(Child) 
export default function Parent(props: any) {
    const [num, setNum] = useState<number>(0)
    const handleClick = () => {
        setNum(num + 1)
    }
    return (
        <div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 引用子組件 */} {/* 修改代碼 */} {/* 父組件傳遞 name 狀態給子組件 */} {/* <MemoChild name={name} /> */} <MemoChild person={{name:"張三"}} /> </div>
    )
}

複製代碼

現象:

每次點擊"更改num"按鈕,控制檯都彈出"子組件渲染了"。 傳遞對象 實際業務中,傳遞的對象確定更復雜,我這裏只是爲了說明只要是個對象,就會致使子組件從新渲染。

緣由跟上面傳遞事件是同樣的,傳遞對象,每次從新渲染都指向新的引用,子組件 react.memo 進行淺比較,會得出傳遞的 props 不等了,因此從新渲染。

useCallback 是用來處理父子傳遞事件的優化,至於傳遞狀態就要用到 useMemo。

useMemo

定義

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

把「建立」函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。

優化

import React, { useState,useMemo } from 'react'
import Child from './child'
const MemoChild = React.memo(Child)
export default function Parent(props: any) {
	const [num, setNum] = useState<number>(0)
	const handleClick = () => {
		setNum(num + 1)
	}
	return (
		<div> <h2>{num}</h2> <button onClick={handleClick}>更改num</button> {/* 採用 useMemo */} <MemoChild person={useMemo(()=>({name:"張三"}),[])} /> </div>
	)
}

複製代碼

現象:

每次點擊"更改num"按鈕,控制檯都不會彈出"子組件渲染了"。 memo

這下能夠放心傳遞狀態跟事件了。

總結

React hook 比起類組件有了更大的靈活度和自由,但同時對開發者要求也更高了。由於 Hooks 使用不恰當很容易出現性能問題。 memo,useMemo,useCallback 都是用來提高性能的。

  1. memo 至關於 shouldComponentUpdate
  2. useCallback 讓 shouldComponentUpdate 能夠正常發揮做用
  3. useMemo 避免頻繁的昂貴計算,固然也能夠用在像本文 props 狀態上

那何時用這幾個 api 呢?

實際上在簡單的應用中,儘可能少用,由於對於簡單應用,就算從新渲染也不會消費多少資源,而採用這幾個 api 時,每次都會對第二個參數進行比較,反而消耗了資源。

固然對於開銷比較大的組件就儘可能用 memo。

相關文章
相關標籤/搜索