hook直译为钩子,是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用
hook是react16.8提出的新特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。主要解决了以下三个问题:
- 在组件之间复用状态很困难: 在之前的react中, 需要借助renderprops和高阶组件等复杂的设计模式来复用,当代码变得很复杂。hook可以在不改变原有的组件结构中将状态逻辑抽离出来。
- 复杂组件变得难以理解: class组件中生命周期函数通常会包含不相关的逻辑(比如:数据获取,事件监听),需容易产生bug。hook将这些相关联的部分拆成更小函数,而并非强制按照生命周期划分。
- class组件学习成本高,理解困难: 写class组件之前需要学习this原理,并且cclass 不能很好的压缩,并且会使热重载出现不稳定的情况。Hook 使你在非 class 的情况下可以使用更多的 React 特性。
常见的hooks
useState
简介
useState用来帮助我们管理state值的hook。在一个函数组件的多次渲染之间,这个state是共享的。
基本用法:
const [state, setState] = useState(initialState);
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
函数式更新:如果新的 state 需要通过使用先前的 state 计算得出,那么可以将计算函数传递给 setState
const [count, setCount] = useState(0);
setCount(prevCount => prevCount - 1)
如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
原理:
声明阶段:创建hook,state初始值为initialState
调用阶段:找到对应的hook(此处是useState)拿到最新的state,返回最新的值。
应用场景
- 保存需要页面重新渲染的状态
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>);
}
- 不要保存需要通过计算得到的值
-
- 从props里传入的值:如果传入的props需要通过计算才能在UI上展示,直接进行计算即可,无需再存入state中。
- 从url中读取的值
- 从localstorage、cookie等浏览器缓存中读取的值
常见的坑
使用 useState 导致了不必要的重新渲染
错误示范: 例如页面有个按钮,点击一次,count值就会加1
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
const handleClick=()=>{
setCount(per=>per+1)
}
return <button onClick={handleClick}>增加</button>;
}
分析:
react中state更新都会触发组件及其子组件重新渲染,但是上面的例子中我们定义的count这个state并没有在render中用到。所以我们每次setCount更改count值时不需要重新渲染。
对此,我们可以利用useRef来修改此类问题。useRef返回一个ref对象,useRef变化不会引发页面渲染
正确示范:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
const countRef = useRef(initialCount)
const handleClick=()=>{
countRef.current += 1
}
return <button onClick={handleClick}>增加</button>;
}
使用过时状态
错误示范: 例如页面有个按钮,点击一次,count值就会增加两次
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
const handleClick=()=>{
setCount(count+1);
setCount(count+1);
}
return <button onClick={handleClick}>增加</button>;
}
分析:
由于,React会对多次连续的setState方法进行合并,即使调用了setCount(count + 1) 2次,但是count也只增加1。
对此,我们可以函数式更新的方法,使用当前状态来计算下一个状态来避免这类问题
正确示范:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
const handleClick=()=>{
setCount(per=>per+1);
setCount(per=>per+1);
}
return <button onClick={handleClick}>增加</button>;
}
useReducer
useReducer是uesState的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
基本用法:
const [state, dispatch] = useReducer(reducer, initialArg, init);
指定初始state
const [state, dispatch] = useReducer(reducer,{count: initialCount});
惰性初始state
function init(initialCount) { return {count: initialCount};}
const [state, dispatch] = useReducer(reducer, initialCount, init);
原理: 基本同useState思想一致,在调用阶段,useState实现的函数与useReduce实现的函数为同一函数
应用场景
- 如果 state 变化非常多,也是建议使用 useReducer,集中管理 state 变化,便于维护
- state逻辑较为复杂并且包含多个子值
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
- 不同的属性捆绑在一起,应该在一个状态对象中管理(比如数据请求)
const reducer = (state, action) => {
...
}
const [{ data, isError, isLoading }, dispatch] = useReducer(
reducer,
{ isLoading: false, isError: false, data: [] }
)
useEffect
简述原理
useEffect主要是用来处理函数式组件的副作用
函数式编程中的纯函数的概念,对应React中为纯组件的概念:
// 相同的props和state永远产生相同的UI
f(props, state) = UI;
所以,在React中,有一些会对这个过程产生影响的或者与进行运算产生UI操作无关的操作,统称为副作用,比如发出http请求,DOM操作,console.log等,useEffect即给了我们一个在函数式组件中执行这些副作用的入口。
useEffect的用法如下:
type Destroy = () => void;
function useEffect(create: () => void | Destroy, deps?: any[]) {}
第一个参数是一个函数create,其返回值为空或者一个Destroy类型的函数;第二个参数是一个任意的数组,作为依赖项。每次渲染后,如果依赖项有变化,则会先执行destroy函数然后执行create函数的函数体。
执行时机
不同场景的执行时机:
useEffect的执行时机可以从两个方面来看:
-
首次渲染mount时,在mount的时候,react会在界面渲染后,执行
create函数体。 -
界面更新update时,会首先检查有没有传
deps:-
如果没有传
deps,则在界面渲染后,先执行destroy函数,再执行create函数体; -
如果传了
deps,则会对其中的依赖项做检查,看与上次渲染对比是否有变化:- 如果没有变化,则在界面渲染后,什么也不执行;
- 如果有变化,则在界面渲染后,先执行
destroy函数,再执行create函数体。
-
使用注意:
useEffect只有在渲染的时候才会执行,因此它的依赖项deps必须是可以引起重渲染或重渲染会改变的值,引起重渲染的值如state、context,重渲染会改变的值如props。如果不是这两类值,如普通变量,则effect不会生效。
源码层面:
在源码层面上,effect是在React的commit阶段进行调度的,但不会立即执行,等待commit阶段完成后,浏览器对commit阶段进行的DOM操作进行布局和绘制后执行。
effect不在commit阶段就执行而是异步执行的原因 —— 如果是同步执行的话,会阻塞浏览器渲染。所以最好不要在useEffect中再执行DOM操作,因为此时浏览器已经完成了渲染工作,再变更DOM会再次进行渲染,可能会出现闪动的效果。
但是即使在 useEffect 被推迟到浏览器绘制之后的情况下,它也能保证在任何新的渲染前启动。React 在开始新的更新前,总会先刷新之前的渲染的 effect。
应用场景
- 进行网络请求
useEffect(() => {
(async () => {
const { data } = await fetchData();
setData(data);
})();
}, []);
注意:不要直接写useEffect(async() => {}, []); useEffect的参数create函数预期是不返回任何值或者返回一个函数destroy,而async函数会返回一个Promise,不符合要求。
- 数据操作
// 根据props中的value对某些参数进行同步 —— 同步数据
useEffect(() => {
generateParams(props.value);
}, [props.value]);
// 根据props中的value对表单进行数据填写 —— 数据预填写
useEffect(() => {
formRef.current.formApi.setValues(transformValue(props.value));
}, [props.value]);
- 其他副作用
// 进行存储相关的工作
useEffect(() => {
localStorage.setItem('state', JSON.stringify(state));
}, [state]);
// 进行事件监听或发布订阅相关工作 —— 官方示例
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect: return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// 在SSR中应用
'错误写法:SSR下,执行FC时,没有DOM'
const [state, setState] = useState(document.visibilityState);
'正确写法:在useEffect中使用'
const [state, setState] = useState();
useEffect(()=>{
setState(document.visibilityState);
}, []);
常见的坑
- 如果在
useEffect中进行的操作会造成DOM变化,则可能会出现闪动现象,如:
/** 1. state变化导致重渲染,重渲染时执行此useEffect
2. 当state为1时,则会触发setState(2),又一次重渲染
3. 连续渲染两次,会导致屏幕出现闪动
*/
useEffect(() => {
if (state === 1) {
setState(2);
}
}, [state]);
解决方法:使用useLayoutEffect代替useEffect
-
依赖项问题:
- 依赖项没写:此时处理一些数据,比如执行
setState,很可能会导致无限循环渲染问题; - 依赖项少写:
useEffect的执行可能会不符合预期; - 依赖项多写:与逻辑无关的或者定值依赖项,无用,且增加对比deps的复杂度;
- 依赖项内容:deps对比遵循浅比较,所以还是尽量写基础数据类型而不是引用值。
- 依赖项没写:此时处理一些数据,比如执行
解决方法:如果有十足的把握,尽量少写依赖项;如果没有把握,则把所有的依赖项都写上。
- 确定组件是否会重渲染。有一些常用组件的逻辑与预想的不一样,如在Modal组件的基础上封装一个组件,我们主观上会认为每次弹出都是一次重新mount的过程,实际并不是,所以在组件中写了
useEffect不一定会如我们所愿,所以要注意
const MyModal = ({ visible }) => {
useEffect(() => {
// 其实只会执行一次,不是每次重新mount
}, []);
useEffect(() => {
// 这样可能更符合预期
}, [visible]);
return (<Modal visible={visible}>
<div>123</div>
</Modal>);
};
解决方法:用一些第三方组件库时多注意 useLayoutEffect
简述原理
与useEffect基本相同,只是在执行时机上有所不同。
执行时机
相比useEffect的区别:
useEffect在React的commit阶段进行调度,在界面渲染后执行(异步)useLayoutEffect在React的commit阶段进行调用(同步)
其他逻辑与useEffect相同。
应用场景
官方文档: 我们推荐你一开始先用 ****
useEffect,只有当它出问题的时候再尝试使用useLayoutEffect。
即useEffect和useLayoutEffect应用场景完全相同,但是优先使用useEffect,当出现问题时,如上述提到的DOM闪动问题,则可改用useLayoutEffect。
原因:useLayoutEffect会在React的工作阶段中同步执行,阻塞浏览器渲染,所以为了更好的用户体验,要优先使用useEffect;也正是因为同步执行,所以将会触发DOM更改的逻辑放到useLayoutEffect中,会使它在渲染前修改DOM,可以做到只渲染一次,不会出现闪动情况。
常见的坑
SSR中无法使用:
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer’s output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See fb.me/react-usela… for common fixes.
在SSR模式下,会出现如下warning信息,提示useLayoutEffect不会在server端执行。
解决方法:
- 使用
useEffect - 做判断:
import { useLayoutEffect, useEffect } from 'react';
const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
useContext
简介
基本用法
useContext可以帮助我们跨越组件层级直接传递变量,避免了在每一个层级手动的传递 props 属性,实现共享,要配合createContext使用。
const value = useContext(MyContext);
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
原理
声明阶段和调用阶段都是调用了同一函数(readContext),readContext 接收一个 context 对象 (React.createContext 的返回值) 并返回该 context 的当前值
应用场景
- 需要层层传递变量
- useContext+useReducer代替redux
常见的坑
需要缓存provider value
错误示范:
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ContainerContext.Provider value={{ state, dispatch }}>
{props.children}
</ContainerContext.Provider>
);
}
分析:如果 Provider 组件还有父组件,当 Provider 的父组件进行重渲染时,Provider 的value 属性每次渲染都会重新创建
正确示范
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({ state, dispatch }), [state]);
return (
<ContainerContext.Provider value={value}>
{props.children}
</ContainerContext.Provider>
);
}
memo不起作用
错误示范:
React.memo(()=> {
const {count} = useContext(ContainerContext);
return <span>{count}</span>
})
分析:用 context 的地方在context发生变化的时候无论如何都会发生重新渲染,所以很多时候会导致 memo 优化实效。
对此,我们可以把context移到外层
正确示范:
const Child = useMemo((props)=>{
....
})
function Parent() {
const {count} = useContext(ContainerContext);
return <Child count={count} />;
}
useRef
简述原理
官方对于 useRef的解释只有一句话,作为 hooks 一员,useRef 可以让开发者引用一个不参与渲染的值。
反之,useState、useCallback、useMemo 等有返回值的 hook都是会参与渲染的
虽然useState和 useRef 用途上有很大差异,但原则上 useRef 可以在 useState 之上实现。
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
如上代码所示,每次返回给 ref 的都是相同的对象,当然这里的 set 函数就不能再使用了。因为 set 函数会触发渲染流程,且会破坏 ref 的保持引用关系。通过set 函数赋值重新渲染后,有些函数因为闭包的特性持有依然是旧的ref 引用,后续执行时获取的值并不符合自己预期。
执行时机
虽然 useRef 返回的值不参与渲染,
应用场景
还是结合官方的示例进行说明,这里官方给出了三个例子。
使用 Ref 引用一个值
代码如下,这是常见的一种用法。
import { useRef } from 'react';
function Stopwatch() {
const intervalRef = useRef(0);
// ...
通过使用 ref,你可以确保:
- 在渲染间隙存储一些信息(直接申明的变量会因为渲染被重新赋值)
- 改变值不会引起重新渲染(对比
useState等) - ref 本身仍是组件副本的一份本地信息(申明在函数组件外的变量是由多个组件副本共享的)
使用 ref 操作 DOM
代码如下,这也是比较常见的一种做法。
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return <input ref={inputRef} />;
}
在给指定dom节点赋值 ref 后,React 在创建该 dom 时会将 ref 的 current 值赋值为该 dom 节点,在该 dom 节点移除后会将 ref 的 current 值赋值为 null。
避免重新创建 ref 内容
这是不推荐的一种做法,代码如下。
function Video() {
const playerRef = useRef(new VideoPlayer());
// ...
这种情况不会破坏 ref 引用值的唯一性,因为 new VideoPlayer() 的值只会作为初始值赋值一次。但是每次重新渲染时,该构造函数仍然会执行,造成不必要的性能浪费。推荐采用如下方式初始化 ref:
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
常见的坑
- 如上述第三个例子所示,反复重新创建 ref 内容,虽然不会影响功能逻辑,但也会造成不必要的性能开销
useRef只用来引用 dom 元素,useRef最常见的使用场景是引用 dom 元素,但绝不是只可以用来引用 dom 元素。ref和ref.current作为其他 hook的依赖,这种用法明显有悖使用useRef的初衷,既想让一个值可变,又想让它不可变,是不现实的。- 在render 时更新 ref,如下所示,将代码逻辑直接放到 render 函数体内,可能会导致代码逻辑不符合预期。采用如上述第三个例子所示的初始化方式是可行的(利用
useRef的懒初始化特性)
const RenderCounter = () => {
const counter = useRef(0);
// counter.current的值可能增加不止一次
counter.current = counter.current + 1;
return (
<h1>{`The component has been re-rendered ${counter.current} times`}</h1>
);
}
代码逻辑直接放到 render 函数体内是一个常见的误区,这里说的 render 函数针对类组件就是类组件内的 render 函数,针对函数组件就是组件函数本身。render 函数不同于一般的函数,在 React 进行一次渲染过程中, render 函数有可能被调用多次(例如启用并发模式后),这时直接写在函数体内并不会符合自己的预期。最好的解决办法就是思考这段代码的实际作用,然后利用 hooks 或者事件监听函数去进行实现。
注意
- 除引用 dom 元素外,不参与 UI 渲染的局部变量可以考虑使用
useRef缓存,避免useState过于臃肿,使代码更直观 - 尽量少用
useRef直接修改 dom ,少用不是不用,多数情况下思考使用数据驱动视图的方式,但是针对Canvas 和 WebGL 等特殊场景该用还是得用 - 延迟调用场景,使用
useRef解决闭包问题,如下代码所示,useEffect中定时器函数在执行时一定输出的是最新的值
function Demo() {
const countRef = useRef(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current)
}, 3000);
return () => {
clearTimeout(timer);
}
}, [])
return (
<button
onClick={() => { countRef.current++ }}
>
click
</button>
)
}
如果某个值一定需要 useState申明,且该值也需要支持延迟调用,可以考虑使用 useEvent 解决
useMemo和useCallback
简述原理
官方关于useMemo的解释如下,useMemo 作为React的一个钩子函数,它可以帮助你在重新渲染之间缓存计算的结果。
useMemois a React Hook that lets you cache the result of a calculation between re-renders.
官方解释比较绕口,其实可以先从字面意思了解一二。memo 意为备忘录,意指任何一种能够帮助记忆,简单说明主题与相关事件的书面资料。相信很多人一定都做过有关回溯、动态规划的题,其中有一个很重要的思想为了减少重复计算,引入了备忘录的概念,将计算过的值存入备忘录,下次需要计算时先查找备忘录,如果有可直接取出,可以极大的减少算法的时间复杂度。
useMemo 的作用与之类似,多次状态的频繁变更极易引起React渲染性能的下降,内部的协调器利用diff算法只是在一个比较大的宏观层面解决了一定的性能问题,所以 React 提供了 useMemo 钩子函数方便开发者结合实际的业务逻辑对代码进行优化。
执行时机
useMemo 的返回值是会参与UI渲染的,所以它的执行时机是在渲染过程中,所以不恰当的使用 useMemo 会极大的影响应用的首屏渲染。
应用场景
官方给出了四个使用useMemo 的案例
避免昂贵的重新计算
import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
如上代码所示,filterTodos 是一项极为耗时的操作,且只在todos和 tab 项更改时才需要重新计算。这时候就可以利用 useMemo 将计算的值缓存下来,下次组件渲染发现有缓存值且无需重新计算则可以直接取缓存值,达到提升性能的目的。
避免重新渲染组件
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
默认情况下,React 在diff过程中判定某一个组件需要重新渲染,那么该组件所有的子组件都会递归的进行重新渲染(不一定会真正重新生成虚拟dom,但会进行 diff 操作,diff 本身也算渲染流程的一部分)。
大部分情况下,这种规则是符合预期的,强一致的保证视图渲染一定符合开发者预期。如上代码所示的 TodoList 组件因为切换主题导致重新渲染,那么它的子组件 List 也会跟随重新渲染。
但如果子组件比较复杂,例如子节点数目极多、嵌套层级太深,这时候 React 为开发者提供了 memo函数进行优化。
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
使用 memo 函数包裹该组件后,React 对该组件进行 diff 操作之前会先比较 props 的值改变前后是否一致,一致则直接跳过该组件的渲染,不一致则按正常流程就行。
记忆另一个钩子函数的依赖
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
如果一个钩子函数(这里以 useMemo 为例)依赖了一个对象,而这个对象是组件内直接生成的,这种情况下,该钩子函数会在每次函数渲染时都会执行。解决方案如下:
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...
使用 useMemo 缓存该对象,该对象便可以正常被其他钩子函数所依赖。
上述案例,searchOptions 对象还依赖了参数 text,所以使用 useMemo 是比较合理的,如果不依赖参数 text,可直接使用 useState 代替。
记忆一个函数
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
如上代码所示,Form 组件使用 memo进行了优化,如果 props 不变则跳过组件渲染,但这时的 handleSubmit 函数和上一个案例的 searchOptions 对象一样,每次渲染该函数都会重新生成,这会破坏 Form 组件的优化。
缓存函数本身的目的不是减少重新生成函数的成本,而是为了避免破坏子组件的优化
解决方案如下:
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
思路和上一个案例大同小异,不过这里嵌套了多层函数,看起来不太优雅,所以 React 提供了一个语法糖 useCallback,代码如下:
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
这两段代码完全等价,useCallback相比较 useMemo 只是避免多嵌套一层函数,其他没有新增任何功能。
常见的坑
- 提前优化,所有组件全部使用 memo 函数,会极大地影响首屏和后续更新渲染的时长,得不偿失
- 子组件使用了 memo 优化,但是 props 参数值没有被 useState或 useMemo 缓存,这种情况相当于没有做任何优化,反而还增加了性能损耗
- 父组件使用useMemo 缓存值直接传递给子组件,子组件没有使用 memo 函数,这种情况和上述的问题相同
注意
- 因为使用 useMemo 有一定的成本,所以在缓存计算值时需要考虑计算量是否达到一定量级,且新旧值的结构、顺序是否有很大不同(V8 引擎做了很多的性能优化,如果生成的新值结构和顺序和旧值保持一致,新建对象成本的优化工作交给 V8 引擎即可)
- 只有在子组件层级很深且逻辑复杂可能会明显增加React 协调器计算时长的时候,再考虑使用 memo 函数优化子组件,其他情况交给 React 内部的协调器优化就好。同时注意,使用 memo 函数优化子组件,一定要保证子组件的入参为useState或 useMemo 返回的值,以保证 memo 的优化工作符合预期。