對於函式元件是否需要再次渲染,可以根據 React.memo 與 React.useMemo 來優化。
函式元件優化 - React.memo
React.memo
React.memo(ReactNode, [(prevProps, nextProps) => {}])
- 第一個引數:元件
- 第二個引數【可選】:自定義比較函式。兩次的 props 相同的時候返回 true,不同則返回 false。返回 true 會阻止更新,而返回 false 則重新渲染。
如果把元件包裝在 React.memo
中呼叫,那麼元件在相同 props 的情況下渲染相同的結果,以此通過記憶元件渲染結果的方式來提高元件的效能表現。
React.memo
僅檢查 props 變更,預設情況下其只會對複雜物件做淺層對比,如果想要控制對比過程,那麼請將自定義的比較函式通過第二個引數傳入來實現。
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
}, (prevProps, nextProps) => {
/*
如果把 nextProps 傳入 render 方法的返回結果與
將 prevProps 傳入 render 方法的返回結果一致則返回 true,
否則返回 false
*/
})
結論
當父元件重新渲染時:
- 子元件未使用 React.memo,不管子元件 props 是什麼型別,子元件都會重複渲染;
子元件使用 React.memo,並且不傳入第二個引數
- 當子元件的 props 是基礎型別,子元件不會重複渲染;
- 當子元件的 props 是引用型別,如果 props 未使用對應的 hook,那麼會重複渲染,並且子元件多一次 diff 計算。如果使用對應的 hook,不會重複渲染;
- 子元件使用 React.memo,並且自定義對比函式,子元件是否重複渲染由自定義函式決定;
測試
背景介紹
在一個父元件裡有兩個子元件,當父元件發生重新渲染時,兩個子元件在不同的條件控制下是否會重新渲染?
欄位解釋
欄位名 | 含義 | 測試意義 |
---|---|---|
title | 基礎型別常量 | 測試基礎型別改變對子元件的影響 |
commonObject | 引用型別常量 | 測試引用型別改變對子元件的影響;測試 hook(useMemo) 定義的引用型別改變對子元件的影響 |
dataSource | useState 定義的引用型別 | 測試 hook(useState) 定義的引用型別改變對子元件的影響 |
updateXxxxInfo | 方法 | 測試引用型別改變對子元件的影響;測試 hook(useCallBack) 定義的引用型別改變對子元件的影響 |
基礎程式碼
子元件 BaseInfo:
const BaseInfo = (props) => {
console.log('BaseInfo 重新渲染, props:', props)
const { title, dataSource = {} } = props
return (
<Card title={title}>
<div>姓名:{dataSource.name}</div>
</Card>
)
}
子元件 OtherInfo:
const OtherInfo = (props) => {
console.log('OtherInfo 重新渲染, props:', props)
const { title, dataSource } = props
return (
<Card title={title}>
<div>學校:{dataSource.school}</div>
</Card>
)
}
父元件 FunctionTest:
function FunctionTest() {
const [baseInfo, setBaseInfo] = useState({ name: '混沌' })
const [otherInfo, setOtherInfo] = useState({ school: '上海大學' })
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Space>
<Button
onClick={() => {
console.log('點選-修改基本資訊')
setBaseInfo({ name: '貔貅' })
}}
>修改基本資訊</Button>
<Button
onClick={() => {
console.log('點選-修改其他資訊')
setOtherInfo({ school: '北京大學' })
}}
>修改其他資訊</Button>
</Space>
<BaseInfo
title="基本資訊 - 子元件"
dataSource={baseInfo}
/>
<OtherInfo
title="其他資訊 - 子元件"
dataSource={otherInfo}
/>
</Space>
)
}
測試一:修改子元件 BaseInfo
為 React.memo 包裹
const BaseInfo = React.memo((props) => {
console.log('BaseInfo 重新渲染, props:', props)
const { title, dataSource = {} } = props
return (
<Card title={title}>
<div>姓名:{dataSource.name}</div>
</Card>
)
})
點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
結論:
- 當 props 是基本型別或 react hook(useState) 定義的引用型別時,使用 React.memo 可以阻止重複渲染。
- 使用了 React.memo 的 BaseInfo,當在 props 相同時沒有重複渲染。
測試二:在測試一的基礎上,在父元件 FunctionTest
中新增引用型別,並傳給兩個子元件
測試2.1: 當引用型別的常量是一個物件/陣列時
function FunctionTest() {
//...
const commonObject = {}
//...
return (
// ...
<BaseInfo
title="基本資訊 - 子元件"
dataSource={baseInfo}
commonObject={commonObject}
/>
<OtherInfo
title="其他資訊 - 子元件"
dataSource={otherInfo}
commonObject={commonObject}
/>
// ...
)
}
點選“修改基本資訊”或“修改其他資訊”,BaseInfo 與 OtherInfo 全部重新渲染。
測試2.2: 當引用型別的常量是一個方法時:
function FunctionTest() {
//...
const updateBaseInfo = () => {
console.log('更新基本資訊,原資料:', baseInfo)
setBaseInfo({ name: '饕餮' })
}
const updateOtherInfo = () => {
console.log('更新其他資訊,原資料:', otherInfo)
setOtherInfo({ school: '河南大學' })
}
//...
return (
//...
<BaseInfo
title="基本資訊 - 子元件"
dataSource={baseInfo}
updateBaseInfo={updateBaseInfo}
/>
<OtherInfo
title="其他資訊 - 子元件"
dataSource={otherInfo}
updateOtherInfo={updateOtherInfo}
/>
//...
)
}
點選“修改基本資訊”或“修改其他資訊”,BaseInfo 與 OtherInfo 全部重新渲染。
結論:
- 當 props 包含引用型別時,使用 React.memo 並且不自定義比較函式時不能阻止重複渲染。
- 無論有沒有使用 React.memo 都會重新渲染,此時 BaseInfo 效能不如 OtherInfo, 因為 BaseInfo 多了一次 diff。
測試三:在測試二的基礎上,新增 hook
測試3.1:給 commonObject 新增 useMemo hook
const commonObject = useMemo(() => {}, [])
點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
測試3.2: 給 updateBaseInfo
與 updateOtherInfo
新增 useCallback hook
const updateBaseInfo = useCallback(() => {
console.log('更新基本資訊,原資料:', baseInfo)
setBaseInfo({ name: '饕餮' })
}, [])
const updateOtherInfo = useCallback(() => {
console.log('更新其他資訊,原資料:', otherInfo)
setOtherInfo({ school: '河南大學' })
}, [])
點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
結論:
- 當 props 的函式使用 useMemo/useCallback 時,使用 React.memo 並且不自定義比較函式時可以阻止重複渲染。
- 使用了 React.memo 的 BaseInfo,當在 props 相同時沒有重複渲染。
測試四:在測試三的基礎上,給 OtherInfo 新增 React.memo 並且自定義比較函式
const OtherInfo = React.memo((props) => {
console.log('OtherInfo 重新渲染, props:', props)
const { title, dataSource, updateOtherInfo } = props
return (
<Card title={title}>
<div>學校:{dataSource.school}</div>
<Button onClick={updateOtherInfo}>更新學校</Button>
</Card>
)
}, (prevProps, nextProps) => {
console.log('OtherInfo props 比較')
console.log('OtherInfo 老的props:', prevProps)
console.log('OtherInfo 新的props:', nextProps)
let flag = true
Object.keys(nextProps).forEach(key => {
let result = nextProps[key] === prevProps[key]
console.log(`比較 ${key}, 結果是:${result}`)
if (!result) {
flag = result
}
})
console.log(`OtherInfo 元件${flag ? '不會' : '會'}渲染`)
return flag
})
點選“修改基本資訊”後,BaseInfo 重新渲染, OtherInfo 沒有重新渲染。
點選“修改其他資訊”後,BaseInfo 沒有重新渲染,OtherInfo 重新渲染。
結論:
- 當 props 的函式使用 useMemo/useCallback 時,使用 React.memo 並且不自定義比較函式時可以阻止重複渲染。
- React.memo 的第二個引數可以判斷是否需要自定義渲染。
函式元件優化 - React.useMemo
React.useMemo
React.useMemo(() => {}, [])
返回一個 memoized 值。
把“建立”函式和依賴項陣列作為引數傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized 值。
如果沒有提供依賴項陣列,useMemo
在每次渲染時都會計算新的值。
結論
當父元件重新渲染時:
- 子元件未使用 React.useMemo,不管子元件 props 是什麼型別,子元件都會重複渲染;
- 子元件使用 React.useMemo,依賴項陣列的值有改變時會造成子元件重複渲染;
測試
React.memo 預設是對 props 淺比較,React.useMemo 是對依賴項陣列淺比較,所以針對不同的引數比較結果相同【這裡就不詳細介紹了】。
引用型別的引數建議使用 useState, useMemo,useCallback 等hooks,否則淺比較結果不同。