在閱讀本文之前,請確保您具有 js 基礎知識,知悉基礎數據類型與復雜數據類型的區別。
如果下面的代碼您不能理解,請略過此文以節約您的時間。
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false
目錄:
- React.memo()
- React.useCallback()
- React.useMemo()
React.memo()
問題
React 中當組件的 props 或 state 變化時,會重新渲染視圖,實際開發會遇到不必要的渲染場景。看個例子:
子組件:
function ChildComp () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
}
父組件:
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
return (
<div>
<button onClick={increment}>點擊次數:{count}</button>
<ChildComp />
</div>
);
}
子組件中有條 console 語句,每當子組件被渲染時,都會在控制臺看到一條打印信息。
點擊父組件中按鈕,會修改 count 變量的值,進而導致父組件重新渲染,此時子組件壓根沒有任何變化(props、state),但在控制臺中仍然看到子組件被渲染的打印信息。
我們期待的結果:子組件的 props 和 state 沒有變化時,即便父組件渲染,也不要渲染子組件。
解決
修改子組件,用 React.memo() 包一層。
這種寫法是 React 的高階組件寫法,將組件作為函數(memo)的參數,函數的返回值(ChildComp)是一個新的組件。
import React, { memo } from 'react'
const ChildComp = memo(function () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
})
覺得上面??那種寫法別扭的,可以拆開寫。
import React, { memo } from 'react'
let ChildComp = function () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
}
ChildComp = memo(ChildComp)
此時再次點擊按鈕,可以看到控制臺沒有打印子組件被渲染的信息了。
(控制臺中打印的那一行值是第一次渲染父組件時,渲染子組件打印的,后面再點擊按鈕重新渲染父組件時,并沒有再重新渲染子組件)
React.useCallback()
問題
可別以為到這里就結束了!
上面的例子中,父組件只是簡單調用子組件,并未給子組件傳遞任何屬性。
看一個父組件給子組件傳遞屬性的例子:
子組件:(子組件仍然用 React.memo() 包裹一層)
import React, { memo } from 'react'
const ChildComp = memo(function ({ name, onClick }) {
console.log('render child-comp ...')
return <>
<div>Child Comp ... {name}</div>
<button onClick={() => onClick('hello')}>改變 name 值</button>
</>
})
父組件:
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const [ name, setName ] = useState('hi~')
const changeName = (newName) => setName(newName) // 父組件渲染時會創建一個新的函數
return (
<div>
<button onClick={increment}>點擊次數:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
父組件在調用子組件時傳遞了 name 屬性和 onClick 屬性,此時點擊父組件的按鈕,可以看到控制臺中打印出子組件被渲染的信息。
React.memo() 失效了???
分析下原因:
- 點擊父組件按鈕,改變了父組件中 count 變量值(父組件的 state 值),進而導致父組件重新渲染;
- 父組件重新渲染時,會重新創建 changeName 函數,即傳給子組件的 onClick 屬性發生了變化,導致子組件渲染;
感覺一切又說的過去,由于子組件的 props 改變了,所以子組件渲染了,沒問題呀!
回過頭想一想,我們只是點擊了父組件的按鈕,并未對子組件做任何操作,壓根就不希望子組件的 props 有變化。
useCallback 鉤子進一步完善這個缺陷。
解決
修改父組件的 changeName 方法,用 useCallback 鉤子函數包裹一層。
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
// 每次父組件渲染,返回的是同一個函數引用
const changeName = useCallback((newName) => setName(newName), [])
return (
<div>
<button onClick={increment}>點擊次數:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
此時點擊父組件按鈕,控制臺不會打印子組件被渲染的信息了。
究其原因:useCallback() 起到了緩存的作用,即便父組件渲染了,useCallback() 包裹的函數也不會重新生成,會返回上一次的函數引用。
React.useMemo()
問題
useMemo 又是干嘛的呢?
前面父組件調用子組件時傳遞的 name 屬性是個字符串,如果換成傳遞對象會怎樣?
下面例子中,父組件在調用子組件時傳遞 info 屬性,info 的值是個對象字面量,點擊父組件按鈕時,發現控制臺打印出子組件被渲染的信息。
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = { name, age } // 復雜數據類型屬性
return (
<div>
<button onClick={increment}>點擊次數:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
分析原因跟調用函數是一樣的:
- 點擊父組件按鈕,觸發父組件重新渲染;
- 父組件渲染,
const info = { name, age }
一行會重新生成一個新對象,導致傳遞給子組件的 info 屬性值變化,進而導致子組件重新渲染。
解決
使用 useMemo 對對象屬性包一層。
useMemo 有兩個參數:
- 第一個參數是個函數,返回的對象指向同一個引用,不會創建新對象;
- 第二個參數是個數組,只有數組中的變量改變時,第一個參數的函數才會返回一個新的對象。
function ParentComp () {
// ....
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = useMemo(() => ({ name, age }), [name, age]) // 包一層
return (
<div>
<button onClick={increment}>點擊次數:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
再次點擊父組件按鈕,控制臺中不再打印子組件被渲染的信息了。