前言
组件中状态(State)发生改变会导致该组件重新渲染,其中的子组件也会被重新渲染。如果子组件中并未使用该状态(State),重复渲染会导致无效的性能损耗。
在阻止重新渲染这个需求的基础上,诞生了memo函数,memo是react的一种缓存技术,这个函数可以检测从父组件接收的props,并且在父组件改变state的时候比对这个state是否是本组件在使用,如果不是,则不会重新渲染子组件。
基本使用
使用 memo
将组件包装起来,以获得该组件的一个 记忆化 版本。通常情况下,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。
import { memo } from “react”
memo(Component, arePropsEqual?)
参数
Component
:要进行记忆化的组件。memo
不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和forwardRef
组件。- 可选参数
arePropsEqual
:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回true
。否则返回false
。通常情况下,你不需要指定此函数。默认情况下,React 将使用Object.is
比较每个 prop。
返回值
memo
返回一个新的 React 组件。它的行为与提供给 memo
的组件相同,只是当它的父组件重新渲染时 React 不会总是重新渲染它,除非它的 props 发生了变化。
案例
未使用memo
Head组件中并未使用count,但setCount后,也发生了重新渲染。
import React, { useState } from "react"
function Head() {
return <div>Head,{Math.random()}</div>
}
function App() {
const [count, setCount] = useState(1)
return (
<>
<div>App,count={count}</div>
<button
onClick={() => {
setCount(count + 1)
}}
>
添加
</button>
<Head></Head>
</>
)
}
点击添加前
点击后,可以发现setState导致子组件也重新渲染。
如果当子组件中逻辑复杂或者父组件会频繁重新渲染,就会带来性能消耗,可以使用memo来对此进行优化。
使用memo
import React, { useState, memo } from "react"
const Head = memo(function Head() {
return <div>Head,{Math.random()}</div>
})
function App() {
const [count, setCount] = useState(1)
return (
<>
<div>App,count={count}</div>
<button
onClick={() => {
setCount(count + 1)
}}
>
添加
</button>
<Head></Head>
</>
)
}
点击前
多次点击发现,子组件并不会重新渲染
如果子组件props也接收了count
这个状态,那么还会再次渲染
。
注意
当props
为复杂数据类型
时,setCount后,App组件重新渲染,此时list的地址发生改变,由于Object.is对比的是两个复杂数据类型的地址,所以子组件的props会被视为已发生改变,导致再次渲染,此时需要使用 useMemo 对计算结果进行缓存。
import React, { useState, memo } from "react"
const Head = memo(function Head() {
return <div>Head,{Math.random()}</div>
})
function App() {
const [count, setCount] = useState(1)
const [msg, setMsg] = useState("Hello World")
const list = [msg.toLowerCase(), msg.toUpperCase()]
return (
<>
<div>App,count={count}</div>
<button
onClick={() => {
setCount(count + 1)
}}
>
添加
</button>
<Head list={list}></Head>
</>
)
}
使用 useMemo 缓存
import React, { useState, memo, useMemo } from "react"
const Head = memo(function Head() {
return <div>Head,{Math.random()}</div>
})
function App() {
const [count, setCount] = useState(1)
const [msg, setMsg] = useState("Hello World")
const list = useMemo(()=>[msg.toLowerCase(), msg.toUpperCase()],[msg])
return (
<>
<div>App,count={count}</div>
<button
onClick={() => {
setCount(count + 1)
}}
>
添加
</button>
<Head list={list}></Head>
</>
)
}
useMemo可以将结果进行缓存,其第一个参数的返回值为需要缓存的数据,第二个参数是一个依赖数组,只有依赖项发生改变时,才会重新计算,否则一直使用缓存值。
当props
为函数
时,useMemo的写法可读性较差,可以使用useCallback
对函数进行缓存。
import React, { useState, memo, useMemo, useCallback } from "react"
const Head = memo(function Head() {
return <div>Head,{Math.random()}</div>
})
function App() {
const [count, setCount] = useState(1)
const [msg, setMsg] = useState("Hello World")
// 使用useMemo
const fn = useMemo(
() => () => {
console.log("fn")
},
[msg]
)
// 使用useCallback
const fn = useCallback(() => {
console.log("fn")
}, [msg])
return (
<>
<div>App,count={count}</div>
<button
onClick={() => {
setCount(count + 1)
}}
>
添加
</button>
<Head fn={fn}></Head>
</>
)
}
总结
被memo函数处理过的组件只有当本身的props改变之后才会重新渲染。
memo源码
源码
function memo(type, compare) {
// 核心代码
return {
$$typeof: REACT_MEMO_TYPE,
type: type,
compare: compare === undefined ? null : compare
};
}
memo
的源码比较简单,在返回一个记忆化组件的同时,给组件添加 $$typeof 标记。在创建 Fiber 时,会根据 $$typeof 的值,给 fiber.tag 赋值为 MemoComponent(MemoComponent=14)。
工作流
当 state(可以是任何组件的 state)改变时,React 会从根节点开始遍历 Fiber 树,在这个过程中会对每一个 Fiber 节点调用 beginWork 方法。当 tag 为 MemoComponent 时,会调用 updateMemoComponent 方法。当我们在使用 memo 没有传入第二个参数时,会调用默认的 shallowEqual 对 prevProps 和 nextProps 进行浅比较,当结果为 true 时,组件就不会重新渲染。
function beginWork(...) {
// ...
switch (workInProgress.tag) {
case MemoComponent: {
return updateMemoComponent(...);
}
}
}
function updateMemoComponent(...) {
// ...
if (updateExpirationTime < renderExpirationTime) {
// Default to shallow comparison
var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current$$1.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(...);
}
}
var newChild = createWorkInProgress(...);
return newChild;
}