详解react hooks

665 阅读20分钟

hook直译为钩子,是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用

hook是react16.8提出的新特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。主要解决了以下三个问题:

  1. 在组件之间复用状态很困难: 在之前的react中, 需要借助renderprops和高阶组件等复杂的设计模式来复用,当代码变得很复杂。hook可以在不改变原有的组件结构中将状态逻辑抽离出来。
  2. 复杂组件变得难以理解: class组件中生命周期函数通常会包含不相关的逻辑(比如:数据获取,事件监听),需容易产生bug。hook将这些相关联的部分拆成更小函数,而并非强制按照生命周期划分。
  3. 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

useEffectuseLayoutEffect应用场景完全相同,但是优先使用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 可以让开发者引用一个不参与渲染的值。

反之,useStateuseCallbackuseMemo 等有返回值的 hook都是会参与渲染的

虽然useStateuseRef 用途上有很大差异,但原则上 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 元素。
  • refref.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的一个钩子函数,它可以帮助你在重新渲染之间缓存计算的结果。

useMemo is 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 只是避免多嵌套一层函数,其他没有新增任何功能。

常见的坑

  1. 提前优化,所有组件全部使用 memo 函数,会极大地影响首屏和后续更新渲染的时长,得不偿失
  2. 子组件使用了 memo 优化,但是 props 参数值没有被 useState或 useMemo 缓存,这种情况相当于没有做任何优化,反而还增加了性能损耗
  3. 父组件使用useMemo 缓存值直接传递给子组件,子组件没有使用 memo 函数,这种情况和上述的问题相同

注意

  1. 因为使用 useMemo 有一定的成本,所以在缓存计算值时需要考虑计算量是否达到一定量级,且新旧值的结构、顺序是否有很大不同(V8 引擎做了很多的性能优化,如果生成的新值结构和顺序和旧值保持一致,新建对象成本的优化工作交给 V8 引擎即可)
  2. 只有在子组件层级很深且逻辑复杂可能会明显增加React 协调器计算时长的时候,再考虑使用 memo 函数优化子组件,其他情况交给 React 内部的协调器优化就好。同时注意,使用 memo 函数优化子组件,一定要保证子组件的入参为useState或 useMemo 返回的值,以保证 memo 的优化工作符合预期。