一. 前置知识
React组件更新方面的一些常识:
1. 当组件内部state发生变化,该组件会重新渲染
export default function Father() {
console.log('父组件重新渲染')
const [value, setValue] = useState(0)
return (
<div>
<div>value: {value}</div>
<hr/>
<button onClick = {()=>{setValue(value+1)}}>value +1</button>
</div>
)
}
非常简单的栗子,当点击按钮,使得组件内部的一个state value变化时,从控制台看到,这个函数组件是会再一次被调用的,就是重新渲染。
2. 父组件的重新渲染,会导致子组件也重新渲染
export default function Child() {
console.log('子组件重新渲染')
return (
<div>
<div>Child组件</div>
</div>
)
}
定义一个子组件,该子组件不依赖与父组件的任何属性。(在父组件内只是展示Child组件)
由控制台看到,第一组[父组件重新渲染、子组件重新渲染] 是当组件第一次渲染时打印的。
当点击父组件中的按钮,使得父组件的状态改变,由1知,父组件是会重新渲染的。但是打印台告诉我们,子组件也重新渲染了。
说明:父组件的state改变-父组件重新渲染-导致子组件也重新渲染
也有🌰证明,即使子组件依赖了父组件的数据,(比如n),但是我没有改变n,改变的是与子组件无关的value,子组件也会跟着重新渲染。
这说明,有很多场景,会导致子组件的无效渲染。也就是说,子组件明明可以不需要重新渲染,只要当它依赖的父组件的数据变化,它再重新渲染就可以了。这就是React.memo的用处
二. React.memo
如果说上述的Child只是一个普通的函数组件,只会跟着父组件的渲染而无脑渲染自身。那么用React.memo包装后的组件就拥有了识别props的能力,它能做到:
只有当它依赖的props变化,它才会重新渲染
function Child() {
console.log('子组件重新渲染')
return (
<div>
<div>Child组件</div>
</div>
)
}
export default React.memo(Child)
我们在Child外包裹一层React.memo,还是走一遍上边的流程
第一组两个组件都渲染,是组件初始渲染打印结果。
当点击按钮,父组件的state改变,父组件重新渲染了,但是子组件没有重新渲染。
Child组件有依赖的props的🌰:
function Child(props) {
console.log('子组件重新渲染')
const {n} = props
return (
<div>
<div>Child组件,n:{n}</div>
</div>
)
}
export default React.memo(Child)
父组件有两个状态:value和n。子组件只依赖n
export default function Father() {
console.log("父组件重新渲染");
const [value, setValue] = useState(0);
const [n, setN] = useState(0);
return (
<div>
<div>value: {value}</div>
<div>n: {n}</div>
<hr />
<button
onClick={() => {
setValue(value + 1);
}}
>
value +1
</button>
<button
onClick={() => {
setN(n + 1);
}}
>
n +1
</button>
<hr />
<Child n={n}/>
</div>
);
}
结果是,只有当点击n+1的按钮后,父组件状态n改变 - 父组件渲染 - n是Child依赖的属性,所以Child也渲染
这就避免了很多与子组件无关的状态变化导致的子组件无效渲染。
三. useCallback
useCallback 也是用来避免子组件无效渲染的,针对函数。
上边虽然给子组件加了一层React.memo,但是如果子组件的一个props是函数,该函数在父组件内部定义,当父组件由于一个状态改变导致重新渲染时,其内部的函数也会被重新创建,虽然函数内容都相同,但它的引用改变了,即该函数fn在内存里指向的地址变了。所以子组件认为这个prop变了,就会重新渲染自己。但其实函数本身并没变,所以这种情况下的无效渲染也可以优化。
🌰 1:
export default function Father() {
console.log("父组件重新渲染");
const click = () => {
console.log('我是点击函数')
}
return (
<div>
<Child n={n} click = {click}/>
</div>
);
}
给父组件额外定义一个函数click,并作为prop传给子组件,子组件同样只依赖n。
结果是,当我点击value+1时,子组件也重新渲染了。
这就是因为父组件的渲染导致click函数被重新创建,使其地址变化。
优化:把要传给子组件的函数包裹在useCallback内:
const click = useCallback(() => {
console.log('我是点击函数')
},[n])
useCallback语法:
- 接受的第一个参数是一个函数
接受的第二个参数是一个数组,表示依赖项:
- 如果数组内有值,表示只有当依赖项变化,返回的那个函数才会重新指向参数函数新的地址
- 如果是空数组,就说明只有组件第一次渲染,才会为返回函数创建引用。其余情况click的地址都不会变化。
- 如果不传,说明只要组件渲染一次,click就会被创建一次,其引用地址就会更新一次。
- 返回一个函数,可以看作是对参数函数的缓存,与参数函数具有相同的功能。
总结:作用就是,保证只有当依赖项改变,返回的函数才会被重新创建,有一个新的地址。
上面的例子,结果是,当我点击父组件的value+1,子组件没有重新渲染。因为click函数依赖的n没变,所以即使它所处的父组件重新渲染了,click函数在内存里的地址还是上次缓存的结果,子组件的prop n click 都没变,它又包裹了React.memo,所以子组件不会渲染。
特点
useCallback的参数函数内部构成了一个闭包,如果这个函数使用了它外部的变量,这个变量又是组件内部的状态,那么在函数里读到的这个变量值有时是不会随时更新的。
export default function Father() {
console.log("父组件重新渲染");
const [n, setN] = useState(0);
const click = useCallback(() => {
console.log('useCallback内读到的n:',n)
},[])
return (
<div>
<div>父组件内部状态n: {n}</div>
<hr />
<button
onClick={() => {
setN(n + 1);
}}
>
父组件n +1
</button>
<hr />
<Child n={n} click = {click}/>
</div>
);
}
function Child(props) {
console.log('子组件重新渲染')
const {n, click} = props
return (
<div>
<div>Child组件,n:{n}</div>
<button onClick = {click}>点击调用click函数</button>
</div>
)
}
export default React.memo(Child)
父组件内部状态n,一个按钮n+1,点击后n+1;还有一个用useCallback缓存的click函数,作为prop传给Child,click函数被调用,就会打印n的值,无依赖项。
子组件有一个按钮,点击后调用父组件的click函数。
我们先点击几次父组件里的n+1,从ui看父组件的内部状态n确实被改变了。
再点击子组件的按钮,打印n的值:
usecallback内读到的n竟然还是初始值0!
这就是因为click的第二个参数是空数组,无依赖项。因此它只会在组件第一次渲染时被创建引用,同时关联组件上下文,作用域等,并进行缓存,此时n的值是0,所以在click内部作用域里n的值会一直是0.
即使后来n的值变了,但是因为click并不会被重新关联引用,它使用的始终是缓存的,所以当click被调用,要打印n的值,打印出来的始终会是初始值。
const click = useCallback(() => {
console.log('useCallback内读到的n:',n)
},[n])
做一处小的修改:让click依赖n。效果就是,每次n变了,click就会被重新指向内部新函数的地址,它内部的作用域也会更新,使用的变量n的值也就会与改变后的n同步。所以调用click函数,打印出的n始终保持新的值。
所以使用useCallback时,要把函数内使用的状态作为依赖项传进去,不然就拿不到新的状态。
三. useMemo
useCallback缓存的是函数,useMemo缓存的是函数的返回值。
它通常用于优化复杂的计算逻辑,比如一个函数返回一个值,通过使用useMemo,限定只有当这个函数的依赖项改变,才重新调用这个函数,得到新的结果值。如果依赖项不变,函数就不会被调用,使用上次的缓存值。
它类似于Vue里的computed计算属性
const sum = useMemo(() => {
let sum = 0
for(let i=0;i<n;i++) {
sum=sum+n
}
},[n])
同useCallback一样,useMemo的第一个参数也是一个函数,第二个参数是可选的依赖项。
useCallback和useMemo在组件第一次渲染时都会执行一次。
如果第二个参数是空数组,以后函数都不会被调用,返回值sum始终是组件第一次渲染时拿到的值
如果没有第二个参数,说明只要组件更新了,函数就被调用一次,sum就被赋值一次
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。