本文可能會讓你感覺到很啰嗦涕蜂,我想盡量線索清晰析校,但是好像有點失敗...
1. 背景
先看下面這段代碼
import React, {Fragment} from 'react'
import { useState, useMemo } from 'react'
// 產(chǎn)品名稱列表
const nameList = ['apple', 'peer', 'banana', 'lemon']
const example = (props) => {
// 產(chǎn)品名稱、價格
const [price, setPrice] = useState(0)
const [name, setName] = useState('apple')
// 假設(shè)有一個業(yè)務(wù)函數(shù) 獲取產(chǎn)品的名字
function getProductName() {
console.log('getProductName觸發(fā)')
return name
}
return (
<Fragment>
<p>{name}</p>
<p>{price}</p>
<p>{getProductName()}</p>
<button onClick={() => setPrice(price+1)}>價錢+1</button>
<button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
</Fragment>
)
}
export default example
現(xiàn)在問幾個問題:
發(fā)生下面幾種情況會重新渲染界面嗎(也就是getProductName
函數(shù)會被觸發(fā))长酗?
- 點擊價錢+1按鈕溪北?
- 點擊修改名字按鈕?
很顯然在進(jìn)行DOM
相關(guān)操作(如setState
)后夺脾,都會觸發(fā)getProductName
函數(shù)之拨,但是我們想知道這個產(chǎn)品的名字,產(chǎn)品的價格怎么變不是我們關(guān)心的咧叭,所以我們需要讓這個函數(shù)只在產(chǎn)品名字改變的時候再觸發(fā)蚀乔,而不是每次重新渲染都觸發(fā)。
不控制重復(fù)渲染容易產(chǎn)生一些奇怪的問題
// 假設(shè)在上面函數(shù)組件里面有一個定時任務(wù) setInterval(() => { console.log('觸發(fā)了定時器') }, 5000)
當(dāng)你點擊修改價格或者修改名字的時候菲茬,每次都會觸發(fā)渲染吉挣,說白了就是將這個函數(shù)組件的函數(shù),再執(zhí)行一次生均,顯然又會添加一個定時器听想, 這時就容易產(chǎn)生內(nèi)存泄漏,當(dāng)然你也可以在組件外部定義一個變量保存定時器
id
2. 使用什么方法解決
2.1 useEffect
马胧?
最開始遇到這個問題時汉买,由于我剛接觸hook
,我認(rèn)為使用useEffect
就能解決
// 將上面的函數(shù)用useEffect包裹,并設(shè)置依賴佩脊,只有name發(fā)生變化的時候才觸發(fā)
useEffect(() => {
getProductName()
}, [name])
我的想法很簡單蛙粘,就是讓getProductName
只對name
有效果垫卤,只有name
修改的時候才會執(zhí)行。
顯然這是一個錯誤的認(rèn)識出牧,我只理解了useEffect
可以設(shè)置依賴穴肘,并沒有理解到副作用到底是什么東西
看下面一段代碼
import React, {Fragment} from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { observer } from 'mobx-react'
const nameList = ['apple', 'peer', 'banana', 'lemon']
const Example = observer((props) => {
const [price, setPrice] = useState(0)
const [name, setName] = useState('apple')
function getProductName() {
console.log('getProductName觸發(fā)')
return name
}
// 只對name響應(yīng)
useEffect(() => {
console.log('name effect 觸發(fā)')
getProductName()
}, [name])
// 只對price響應(yīng)
useEffect(() => {
console.log('price effect 觸發(fā)')
}, [price])
return (
<Fragment>
<p>{name}</p>
<p>{price}</p>
<p>{getProductName()}</p>
<button onClick={() => setPrice(price+1)}>價錢+1</button>
<button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
</Fragment>
)
})
export default Example
- 先看看
useEffect
的工作順序,若點擊修改名字按鈕會打印什么舔痕?
> getProductName觸發(fā)
> name effect 觸發(fā)
> getProductName觸發(fā)
官方文檔有說過 當(dāng)你調(diào)用 useEffect 時评抚,就是在告訴 React 在完成對 DOM 的更改后運(yùn)行你的“副作用”函數(shù)
所以這個順序很好理解
- 因為修改了名字,然后
react
更改了DOM
伯复,觸發(fā)了getProductName
- 隨后調(diào)用了
name
的effect
(在dom
更新之后觸發(fā)慨代,這也是為什么叫做副作用) -
effect
中調(diào)用了getProductName
- 看看問題解決沒有,若點擊價錢+1按鈕會打印什么啸如?
> getProductName觸發(fā)
> price effect 觸發(fā)
我改變的是價格侍匙,還是觸發(fā)了getProductName
稍微分析:
- 顯然當(dāng)我使用
setPrice
的時候,產(chǎn)生DOM
操作叮雳,刷新頁面DOM
的同時也想暗,觸發(fā)了在p
標(biāo)簽中的getProductName
函數(shù) - 然后調(diào)用副作用觸發(fā)了
price
的effect
就如前面我所提出的問題,我們的目標(biāo)是在DOM
發(fā)生變化時帘不,不相關(guān)的函數(shù)不需要觸發(fā)(也就是這里的getProductName
在我修改價格的時候不應(yīng)該觸發(fā))说莫,而useEffect
只能在DOM
更新后再觸發(fā)再去控制,所以這個馬后炮并沒有什么??用
2.2 useMemo
厌均?
使用useMemo
可以解決這個問題
為什么useMemo
可以解決唬滑?官方文檔說過傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行告唆,所以使用useMemo
就能解決之前的問題棺弊,怎么在DOM
改變的時候,控制某些函數(shù)不被觸發(fā)擒悬。
和useMemo
相近的還有一個useCallback
模她,只是后者返回一個函數(shù)useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)
,這里我們返回一個memo
函數(shù)懂牧,
在上面的代碼中加入useMemo
作對比
import React, {Fragment} from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { observer } from 'mobx-react'
const nameList = ['apple', 'peer', 'banana', 'lemon']
const Example = observer((props) => {
const [price, setPrice] = useState(0)
const [name, setName] = useState('apple')
function getProductName() {
console.log('getProductName觸發(fā)')
return name
}
// 只對name響應(yīng)
useEffect(() => {
console.log('name effect 觸發(fā)')
getProductName()
}, [name])
// 只對price響應(yīng)
useEffect(() => {
console.log('price effect 觸發(fā)')
}, [price])
// memo化的getProductName函數(shù) ??????
const memo_getProductName = useMemo(() => {
console.log('name memo 觸發(fā)')
return () => name // 返回一個函數(shù)
}, [name])
return (
<Fragment>
<p>{name}</p>
<p>{price}</p>
<p>普通的name:{getProductName()}</p>
<p>memo化的:{memo_getProductName ()}</p>
<button onClick={() => setPrice(price+1)}>價錢+1</button>
<button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
</Fragment>
)
})
export default Example
同樣兩個問題
- 點擊價錢+1按鈕會發(fā)生什么
> getProductName觸發(fā)
> price effect 觸發(fā)
- 首先
DOM
改變侈净,觸發(fā)在p
標(biāo)簽中的getProductName
函數(shù) - 然后調(diào)用
effect
顯然我們已經(jīng)成功的控制了觸發(fā)(修改了顯示price
的dom
,但是沒有觸發(fā)memo_getProductName
僧凤,沒有輸出''name memo 觸發(fā)'')畜侦,
這也是官方為什么說不能在useMemo
中操作DOM
之類的副作用操作,不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作躯保,諸如副作用這類的操作屬于 useEffect 的適用范疇旋膳,而不是 useMemo,你可以試一下途事,在useMemo
中使用setState
你會發(fā)現(xiàn)會產(chǎn)生死循環(huán)验懊,并且會有警告擅羞,因為useMemo
是在渲染中進(jìn)行的,你在其中操作DOM
后义图,又會導(dǎo)致memo
觸發(fā)
- 點擊修改名字按鈕會發(fā)生什么
> name memo 觸發(fā)
> getProductName觸發(fā)
> name effect 觸發(fā)
> getProductName觸發(fā)
- 首先
DOM
變化减俏,觸發(fā)name
的memo
, - 然后觸發(fā)
p
標(biāo)簽內(nèi)的getProductName
函數(shù) -
DOM
操作結(jié)束后觸發(fā)name
的effect
- 在
name
的effect
中觸發(fā)getProductName
從這里也可以看出碱工,memo
是在DOM
更新前觸發(fā)的娃承,就像官方所說的,類比生命周期就是shouldComponentUpdate