一、前言
渲染(Render)
Render基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。
react 处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。会将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。
何时触发渲染(Render)
- 组件挂载
- setState() 方法被调用 ( 当 setState 传入 null 的时候,并不会触发 render )
二、React.memo()
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [ name, setName ] = useState('父组件');
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<ChildComponent name={name} />
</div>
);
}
// 子组件
import React, { useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
export default ChildComponent;
子组件中有条 console.log('Child')
语句,每当子组件被渲染时,都会在控制台看到一条打印信息。
这时点击父组件中的 button,会修改 count 变量的值,触发父组件render,此时子组件没有任何变化(props、childCount),但在控制台中仍然看到子组件被渲染的打印信息。
问题:组件在相同 props 的情况下只渲染相同的结果一次,即便父组件渲染,也不要渲染子组件。
解决:子组件用 React.memo()
包裹,如果组件在相同 props 的情况下渲染相同的结果,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
// 子组件
import React, { memo, useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
// 子组件用 React.memo() 包裹
export default memo(ChildComponent);
此时再次点击按钮,可以看到控制台没有打印子组件被渲染的信息了。
React.memo
仅检查 props 变更。如果函数组件被React.memo
包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
三、React.useCallback()
上面的例子中,父组件只是简单调用子组件,并未给子组件传递任何属性。
看一个父组件给子组件传递属性的例子。
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
// 父组件渲染时会创建一个新的函数
const changeName = (el) => setName(el)
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent name={name} changeName={changeName} />
</div>
);
}
// 子组件
import React, { memo, useState } from 'react';
const ChildComponent = ({ name }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => changeName('子组件')}>changeName</button>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
// 仍然用 React.memo() 包裹
export default memo(ChildComponent);
这时点击父组件中的 button,会修改 count 变量的值,触发父组件render,此时调用子组件时 传递了 name 属性和 onClick 属性,在控制台中会看到子组件被渲染的打印信息。
原因:父组件 render 时,会重新创建 changeName()
函数,此时子组件接收的 props
发生改变,所以子组件 React.memo()
检查 props 变更也会 render。
解决:父组件的 changeName()
方法,用 useCallback
钩子函数包裹一层,会把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized
版本,该回调函数仅在某个依赖项改变时才会更新。
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
// 父组件渲染时会创建一个新的函数
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent name={name} changeName={changeName} />
</div>
);
}
- 实际上,被
useCallBack
包裹了的函数changeName()
也会被重新构建并当成useCallBack
函数的实参传入。 useCallBack
的本质不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack
都无法阻止组件render时函数的重新创建。- 每一个被
useCallBack
的函数都将被加入useCallBack
内部的管理队列。而当我们大量使用useCallBack
的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack
的组件重新渲染的时候都需要去便利useCallBack
内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。
三、React.useMemo()
上面例子中,父组件调用子组件时传递的 name
属性是个字符串,若将其换成传递对象:
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const obj = { name: '父组件', name2: '子组件' };
// 父组件渲染时会创建一个新的函数
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent obj={obj} name={name} changeName={changeName} />
</div>
);
}
// 子组件
import React, { memo, useState } from 'react';
const ChildComponent = ({ name, obj }) => {
console.log('Child');
const [childCount, setChildCount] = useState(0);
return (
<div>
<button onClick={() => changeName('子组件')}>changeName</button>
<button onClick={() => setChildCount(childCount + 1)}>
childCount++ :{childCount}
</button>
</div>
)
}
// 仍然用 React.memo() 包裹
export default memo(ChildComponent);
影响:点击父组件按钮,父组件 render 时,const obj = { name: '父组件', name2: '子组件' }
一行会重新生成一个新对象,导致传递给子组件的 obj 属性值变化(新的存储地址),进而导致子组件重新渲染。
解决:把“obj 对象属性作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。有助于避免在每次渲染时都进行高开销的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
export default function IndexPage() {
const [name, setName] = useState('父组件');
const [count, setCount] = useState(0);
const obj = useMemo(() => ({ name: '父组件', name2: '子组件' }), [name])
// 父组件渲染时会创建一个新的函数
const changeName = useCallback((el) => setName(el), [])
return (
<div>
<button onClick={() => setCount(count + 1)}>
count++: {count}
</button>
<br />
<p>{name}</p>
<ChildComponent obj={obj} name={name} changeName={changeName} />
</div>
);
}
再次点击父组件按钮,控制台中不再打印子组件被渲染的信息了。
useMemo是不是用的越多越好?
答案肯定不是
在组件进行渲染并且此组件内使用了
useMemo
之后,为了校验改组件内被useMemo保护的这个计算属性是否需要重新计算,它会先去useMemo
的工作队列中找到这个函数,然后还需要去校验这个函数都依赖是否被更改。(寻找到需要校验的计算属性和进行校验这两个步骤都需要成本)
当我们大量的使用useMemo
之后,非但不能给项目带来性能上的优化,反而会为项目增加负担,我们将这种情况戏称为:反向优化。
- useMemo是用来缓存计算属性的,它会在发现依赖未发生改变的情况下返回旧的计算属性值的地址。
- useMemo绝不是用的越多越好,缓存这项技术本身也需要成本。
- useMemo的使用场景之一是:只需要给拥有巨大计算量的计算属性缓存即可。
- useMemo的另一个使用场景是:当有计算属性被传入子组件,并且子组件使用了
React.memo
进行了缓存的时候,为了避免子组件不必要的渲染时使用。