当面试官问你在使用React开发做过哪些优化时,你可以这么回答

1、使用 React.memo() 缓存组件

React.memoReact 提供的一个高阶组件(Higher Order Component),用于优化函数组件的性能。它可以帮助避免不必要的重新渲染,从而提升应用性能。

函数组件在每次渲染时都会生成新的函数,这可能导致在某些情况下进行不必要的渲染。React.memo 的作用就是在函数组件之间进行浅比较,如果组件的 props 没有发生变化,则避免重新渲染组件。

React.memo 接受一个函数组件作为参数,并返回一个经过优化的组件。下次父组件重新渲染时,如果传递给 MemoizedComponentprops 没有发生变化,该组件将不会重新渲染,从而节省性能。

需要注意以下几点:

  • React.memo 默认使用浅比较,所以它只能检测 props 的值是否相等,无法检测 props 内部的深层次变化。
  • 如果函数组件的 props 是引用类型(如对象、数组),并且它们在每次渲染时都是新的实例,React.memo 可能无法正常工作,此时可以使用 useMemo 来确保传递给子组件的 props 是相同引用。
  • 可以使用第二个参数(一个自定义的比较函数,包含两个参数分别是旧 props 和新 props)来进行更精细的比较,以满足特定的优化需求。

举例🌰:

js 复制代码
const DemoComponent = (props) => <div> {props.value} </div>;

const MemoizedComponent = React.memo(DemoComponent, (prevProps, nextProps) => {
  // 返回 true 表示 props 相等,不重新渲染
  // 返回 false 表示 props 不相等,重新渲染
  return prevProps.value === nextProps.value;
});

React.memoPureComponentshouldComponentUpdate 的区别

  • React.memo 是一个高阶组件(Higher-Order Component),用于包装函数组件。它通过对组件的浅层比较来避免不必要的重新渲染。当组件的 props 没有发生变化时, React.memo 会返回之前渲染的结果,从而跳过组件的重新渲染。React.memo 接受一个可选的比较函数作为第二个参数,用于自定义 props 的比较逻辑。

    React.memo 使用说明:

    • 默认情况下会对组件 props 进行浅比较, 只有 props 变更才会触发 render

    • 允许传入第二参数,该参数是个函数,该函数接收 2 个参数,两个参数分别是旧新 props

    • 注意:与 shouldComponentUpdate 不同的是,返回 true 时,不会触发 render。如果返回 false 则会重新渲染。和 shouldComponentUpdate 刚好与其相反。

  • PureComponent 是一个基于类的组件,它是 React.Component 的一个变种。PureComponent 会进行浅比较 ,通过判断 propsstate 是否相同,来决定是否重新渲染组件。所以一般用于性能调优 ,减少 render 次数。

    PureComponent 使用说明:

    • PureComponent 通过对 propsstate 的浅层比较来实现 shouldComponentUpdate 方法。

    • 由于 PureComponent 默认实现了 shouldComponentUpdate 方法,所以无需手动编写。

  • shouldComponentUpdateReact 组件的生命周期方法之一,它需要手动实现。在 shouldComponentUpdate 方法中,你可以根据组件的 propsstate 来决定是否触发组件的重新渲染。你需要手动比较 propsstate 的变化,并返回一个布尔值,指示组件是否应该更新。

    shouldComponentUpdate 使用说明:

    • propsstate 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。目前,如果 shouldComponentUpdate 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate() 方法。后续版本,React 可能会将 shouldComponentUpdate() 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

    • shouldComponentUpdate 方法接收两个参数 nextPropsnextState,可以将 this.propsnextProps 以及 this.statenextState 进行比较,并返回 false 以告知 React 可以跳过更新。

React.memoPureComponent 作用类似,可以用作性能优化。React.memo 是高阶组件,函数组件和类组件都可以使用。但 React.memo 只能对 props 的情况确定是否渲染,而 PureComponent 是针对 propsstate

React.memo 接受两个参数,第一个参数是原始组件本身,第二个参数可以根据一次更新中 props 是否相同来决定原始组件是否重新渲染。是一个返回布尔值,true 证明组件无须重新渲染,false证明组件需要重新渲染,这个和类组件中的 shouldComponentUpdate() 正好相反。

PureComponent 举例🌰:

js 复制代码
import React, { PureComponent } from 'react';

class Counter extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  incrementCount = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };

  render() {
    console.log('Counter component rendered');
    return (
      <div>
        <h1>Counter: {this.state.count}</h1>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}

function App() {
  return (
    <div>
      <h1>PureComponent Demo</h1>
      <Counter />
    </div>
  );
}

export default App;

在上面的示例中,Counter 组件继承了 PureComponent 类。当点击Increment 按钮时,count 状态会递增。由于 CounterPureComponent,它会对 propsstate 进行浅层比较,只有在它们的值发生实际变化时才会重新渲染。console.log 语句用于显示组件何时被渲染。

shouldComponentUpdate 举例🌰:

js 复制代码
import React, { Component } from 'react';

class MyComponent extends Component {
  state = {
    count: 0
  };

  shouldComponentUpdate(nextProps, nextState) {
    // 仅在count发生变化时才重新渲染组件
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  incrementCount = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}

在上面的示例中,MyComponent 是一个简单的计数器组件,它有一个状态 count,用于存储计数值。在 shouldComponentUpdate 方法中,我们使用比较当前状态和下一个状态的 count 值,如果它们不相等,就返回 true,表示组件应该重新渲染;否则返回 false,表示组件不需要重新渲染。

总结:

  • React.memo 适用于函数组件,通过浅层比较 props 来避免不必要的重新渲染。第二个参数返回 true 组件不渲染,返回 false 组件重新渲染。

  • PureComponent 适用于类组件,通过浅层比较 propsstate 来避免不必要的重新渲染。

  • shouldComponentUpdate 需要手动实现,适用于类组件,可以更细粒度地控制组件的重新渲染,包括 propsstate 的比较逻辑。返回 true 组件渲染,返回 false 组件不渲染。

2、使用 useMemo() 缓存计算值

useMemoReact 提供的一个钩子,用于在函数组件中进行性能优化。它的作用是用于缓存和记忆计算结果,避免在每次渲染时重复计算相同的值,从而提升组件的性能。

useMemo 的基本语法如下:

jsx 复制代码
const memoizedValue = useMemo(() => {
  // 计算或处理逻辑
  return computedValue;
}, [dependencies]);
  • 第一个参数是一个回调函数,该函数在每次渲染时都会被执行。你可以在这个函数中进行计算、处理逻辑或任何其他操作,然后返回计算结果。
  • 第二个参数是一个依赖数组,包含影响计算结果的变量或值。当依赖数组中的值发生变化时,useMemo 会重新计算计算结果。如果依赖数组为空,则只在首次渲染时计算一次。

使用 useMemo 的主要场景是在渲染期间进行昂贵的计算,以及避免不必要的重复计算。以下是一些应用场景:

  • 避免重复计算 :如果一个计算耗时较长且计算结果不会频繁变化,你可以使用 useMemo 来缓存结果,避免在每次渲染时都重新计算。
  • 优化子组件渲染 :将计算结果作为 props 传递给子组件,可以避免在子组件渲染过程中重复执行相同的计算。
  • 避免不必要的副作用 :如果你需要在渲染期间执行副作用(如数据请求、订阅等),你可以在 useMemo 中进行处理,以确保副作用只在依赖项变化时执行。

3、使用 useCallback() 缓存回调函数

useCallbackReact 提供的一个钩子,用于优化函数组件的性能。它的主要作用是缓存函数,避免在每次渲染时重新创建新的函数实例,从而减少不必要的重新渲染。

在使用 useCallback 时,你可以将一个函数和一个依赖数组作为参数传递给它,它会返回一个经过优化的函数。这个函数会在依赖数组中的值发生变化时重新创建,否则会保持相同的引用。

基本语法如下:

jsx 复制代码
const callbackFunction = () => {
  // 回调函数逻辑
}

const memoizedCallback = useCallback(callbackFunction, [dependencies]);
  • callbackFunction:需要缓存的回调函数。
  • dependencies:一个数组,包含影响回调函数是否需要重新创建的值。当数组中的值发生变化时,useCallback 会重新创建回调函数。如果依赖数组为空,回调函数只会在首次渲染时创建一次。

使用场景:

  • 避免子组件不必要的重新渲染 :将回调函数传递给子组件时,可以使用 useCallback 来确保子组件不会因为父组件重新渲染而重新创建回调函数,从而避免不必要的重新渲染。
  • 避免不必要的副作用 :如果回调函数包含副作用(如数据请求、订阅等),使用 useCallback 可以确保副作用只在依赖项变化时执行。

需要注意的是,虽然 useCallback 可以优化性能,但过度使用它可能会导致代码变得复杂。下面说说使用 useCallback() 时可能会存在的缺陷:

  • 内存消耗 :使用 useCallback() 会导致函数的缓存,这意味着每当依赖项发生变化时,都会创建一个新的函数引用。如果在渲染期间频繁地创建大量的函数,可能会增加内存消耗。
  • 比较复杂的逻辑useCallback() 适用于缓存简单的回调函数,但如果需要缓存具有复杂逻辑的函数,可能会导致代码变得难以理解和维护。
  • 额外的函数调用 :使用 useCallback() 可以避免在每次渲染时重新创建函数,但在某些情况下,这可能会导致额外的函数调用。因为记忆化函数的引用发生变化时,使用该函数的组件可能会重新渲染,即使依赖项没有真正发生变化。

拓展知识:

useCallbackuseMemo 的返回值为函数时的特殊情况,是 React 提供的便捷方式。在 React Server Hooks 代码 中,useCallback 就是基于 useMemo 实现的。尽管 React Client Hooks 没有使用同一份代码,但 useCallback 的代码逻辑和 useMemo 的代码逻辑仍是一样的。

使用 useMemo 来实现类似 useCallback 的功能:

js 复制代码
const memoizedFn = useMemo(() => fn, [fn]);

相比较 useCallback,更推荐使用 useMemo

避免使用匿名函数

js 复制代码
// 不推荐
<MyComponent onClick={() => console.log('Clicked')} />

React 组件传值时,避免使用匿名函数是出于性能的考虑。匿名函数在每次渲染时都会重新创建一个新的函数实例,这可能会导致不必要的重新渲染和性能下降。使用匿名函数会导致的问题:

  • 重新渲染的触发 :每次父组件重新渲染时,如果传递给子组件的 prop 中包含匿名函数,那么这些匿名函数都会被重新创建。这会导致子组件的重新渲染,即使其他 prop 没有变化。
  • 额外的内存开销:匿名函数的创建会导致额外的内存开销,因为每次渲染都会生成新的函数实例,这可能在大型应用中累积成性能问题。
  • 性能下降:由于匿名函数的频繁创建和销毁,可能会导致页面的性能下降,尤其是在需要频繁渲染的情况下。

为了避免这些问题,推荐的做法是将匿名函数移到父组件之外,或者使用 useCallback Hook 来缓存函数,从而确保函数实例在渲染之间保持稳定。这样可以避免不必要的重新渲染,并提升组件的性能。

使用具名函数:

js 复制代码
const handleClick = () => console.log('Clicked');

<MyComponent onClick={handleClick} />

使用 useCallback

js 复制代码
import React, { useCallback } from 'react';

function ParentComponent() {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <MyComponent onClick={handleClick} />;
}

4、使用 useRef() 避免非必要渲染

useRefReact 提供的一个钩子,用于在函数组件中存储和访问可变的引用值。与 useState 不同,useRef 不会引发组件的重新渲染,因为它的主要目的是用于处理与界面渲染无关的数据。

基本用法:

jsx 复制代码
const refContainer = useRef(initialValue);
  • refContainer:一个包含 current 属性的对象,current 属性的值为 initialValue
  • initialValue:作为初始值传递给 useRef 的值。

用途:

  • 保留引用useRef 用于保留引用,例如保存 DOM 元素的引用或其他不需要触发重新渲染的值。
  • 获取最新值 :由于 useRef 的值在重新渲染之间保持稳定,它常用于存储任何需要在渲染周期之间保持不变的数据。
  • 触发非渲染副作用useRef 还可以用于触发非渲染的副作用,例如获取上一次渲染的状态或引用。

示例:

jsx 复制代码
import React, { useRef, useEffect } from 'react';

function Timer() {
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => {
      clearInterval(intervalRef.current);
    };
  }, []);

  return <div>Timer component</div>;
}

在上面的示例中,intervalRef 用于存储定时器的引用,以便在组件卸载时清除定时器。由于 useRef 的值在重新渲染之间保持稳定,即使组件重新渲染,intervalRef 的值仍然是上一次渲染时的引用。

5、使用 useLayoutEffect() 避免闪屏

如果你在 useEffct 的初始化渲染中修改了展示的数据或者 css 样式,那么很有可能会重复渲染导致闪屏用户体验不好的问题,此时可以试下 useLayoutEffect 这个钩子。

useLayoutEffectReact 提供的一个钩子,它类似于useEffect,但在组件渲染完成后,但在浏览器执行下一次绘制之前同步调用副作用函数。

useLayoutEffect 的作用和用法与 useEffect 非常相似,都是用于处理组件副作用。然而,它们之间有一个重要的区别: useEffect 是在组件渲染完成后异步调用副作用函数,而 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前同步调用副作用函数。这样做的好处是可以更加方便的修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,可以避免浏览器再次回流和重绘。

useLayoutEffectuseEffect 之前执行。

由于 useLayoutEffect 的副作用函数是同步执行的,它会阻塞浏览器的渲染过程。这意味着如果在 useLayoutEffect 中进行了 DOM 操作或读取 DOM 布局信息,它会在浏览器进行下一次绘制之前立即生效。这使得 useLayoutEffect 适合执行需要 DOM 布局信息的副作用操作,例如测量元素的尺寸或位置,并根据结果进行相应的操作。

举例🌰:

jsx 复制代码
import React, { useState, useLayoutEffect } from 'react';

function MyComponent() {
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    function updateWidth() {
      const element = document.getElementById('my-element');
      if (element) {
        setWidth(element.offsetWidth);
      }
    }

    window.addEventListener('resize', updateWidth);
    updateWidth();

    return () => {
      window.removeEventListener('resize', updateWidth);
    };
  }, []); // 依赖项数组为空,只在组件挂载和卸载时执行一次

  return (
    <div>
      <p>Width: {width}px</p>
      <div id="my-element">Some content</div>
    </div>
  );
}

在上面的示例中,useLayoutEffect 用于测量元素的宽度,并在窗口大小发生变化时更新宽度。副作用函数中使用了 DOM 操作来获取元素的宽度,并使用 useState 来更新组件的状态。

6、正确使用列表 key

React 中,使用 key 属性来标识列表中的每个元素是非常重要的。key 属性帮助 React 跟踪每个列表项的变化,从而优化性能和重新渲染,避免一些不必要的组件树销毁和重建工作。如果不正确地使用 key,可能会导致一些不良的效果,例如错误的渲染和性能下降。

比如你的第一个元素是 div,更新后发生了位置变化,第一个变成了 p。如果你不通过 key 告知新位置,React 就会将 div 下的整棵树销毁,然后构建 p 下的整棵树,非常耗费性能。

正确使用 key 的好处:

  • 元素标识key 属性用于唯一标识列表中的每个元素。它帮助 React 判断哪些元素是新增的、更新的还是被删除的。
  • 优化渲染key 允许 React 在重新渲染列表时,仅更新发生变化的部分,而不必重新渲染整个列表。这可以提升性能,减少不必要的 DOM 操作。
  • 避免错误渲染 :如果不使用 key 或者使用相同的 keyReact 可能会产生不正确的渲染结果,因为它无法正确识别元素的变化。

使用索引(index)作为 key 可能会导致的问题:

  • 错误更新 :如果列表中的项发生位置变化,使用索引作为 key 可能会导致 React 错误地更新了列表项,使得列表项的状态和 UI 不匹配。
  • 性能下降 :使用索引作为 key 可能会导致 React 频繁重新创建元素,降低性能,因为 React 无法正确识别元素的变化。
  • 不稳定的 UI :使用索引作为 key 可能会导致列表项在更新时出现不稳定的 UI,因为它们的 key 是根据位置而不是唯一标识来生成的。

什么情况下能使用数组的 index 值作为 key

  • 列表只会渲染一次
  • 列表的元素不会发生重排

React key 的妙用

在不同的渲染周期去改变组件的 key 值,能够卸载旧的组件实例,重新创建新的组件实例。利用这一点,我们能够实现 组件状态重置正确的数据更新 等效果。

React key 并不只能用于列表渲染场景上,它也可以用于单个组件的渲染场景上!Reactreconcil 过程中,如果当前渲染周期的 key 值跟上一轮渲染周期的 key 值不相等的话,React 会卸载当前组件实例,重新从头开始创建一个新的组件实例。以下 demo 示例就可以验证这一点:

js 复制代码
import React, { useState, useEffect } from 'react';

function Counter() {
  console.log('Counter called');

  const [count, setCount] = React.useState(() => {
    console.log('Counter useState initializer');
    return 0;
  });
  
  const increment = () => setCount((c) => c + 1);

  React.useEffect(() => {
    console.log('Counter useEffect callback');
    return () => {
      console.log('Counter useEffect cleanup');
    };
  }, []);

  console.log('Counter returning react elements');
  
  return <button onClick={increment}>{count}</button>;
}

export default function CounterParent() {
  const [counterKey, setCounterKey] = React.useState(0);
  
  return (
    <div>
      <button onClick={()=> {setCounterKey(c=> c +1)}}>reset</button>
      <Counter key={counterKey} />
    </div>
  );
}

先点击 <Counter> 组件的按钮,再点击 <CounterParent> 组件的按钮,控制台的打印如下:

js 复制代码
// 点击 <Counter> 组件的按钮
Counter called
Counter returning react elements

// 点击 <CounterParent> 组件的按钮
// 组件开始渲染
Counter called
Counter useState initializer
Counter returning react elements

// 卸载旧的组件实例
Counter useEffect cleanup

// 新的组件实例已经挂载到 fiber 节点上
Counter useEffect callback

主动去改变组件的 key 属性值,我们能够达到 卸载旧的组件实例和 DOM 对象,重新创建新的组件实例和 DOM 对象 的效果。利用这一点,我们可以实现类似上面的 状态重置 类型的任务。

还有某些情况,我们在同一个组件上去更新不同的数据的时候,你会发现更新失效,界面还是显示上一次的旧数据。如果事发紧急,那么我们就可以一个能够区分不同渲染周期的 ID值 作为这个组件的 key 值。通过这样,我们就会重新挂载组件实例,从而达到预期的组件更新效果。

7、使用 React.Fragment 避免添加额外的 DOM

React.FragmentReact 提供的一个组件,用于在不引入多余的 DOM 元素的情况下包裹多个子元素。它的主要作用是在 JSX 中创建一个"虚拟"的父容器,用于包裹多个子元素,而不会在最终渲染的 DOM 结构中添加额外的节点。

React 中,当你需要在 JSX 中返回多个相邻的元素时,通常需要将它们包裹在一个父元素中。例如:

jsx 复制代码
<div>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</div>

但是,有时你可能不想在渲染的 DOM 结构中添加多余的父元素。这就是使用 React.Fragment 的情况:

jsx 复制代码
<React.Fragment>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</React.Fragment>

<React.Fragment> 可以像普通的组件一样传递属性,例如 <React.Fragment key={...}>

或者使用简化的语法:

jsx 复制代码
<>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</>

<>...</> 空标签语法不支持传递属性,因此无法传递 key 属性

React.Fragment 的优点包括:

  • 减少 DOM 结构 :使用 React.Fragment 可以避免在最终渲染的 DOM 中添加多余的父元素,减少不必要的 DOM 结构。
  • 不影响样式和布局 :由于 React.Fragment 不会在渲染中生成额外的父元素,因此它不会影响样式和布局。
  • 提高性能 :不生成额外的 DOM 元素,有助于提高性能,特别是在需要渲染大量子元素时。
  • 语法简洁 :使用简化的语法(<>...</>)可以使代码更加简洁,易于阅读。

8、避免使用内联对象

使用内联对象时,React 会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象。因此,该组件对于 prop 的浅层比较始终返回 false,导致组件一直重新渲染。

许多人使用的内联样式的间接引用,就会使组件重新渲染,可能会导致性能问题。为了解决这个问题,我们可以保证该对象只初始化一次,指向相同引用。另外一种情况是传递一个对象,同样会在渲染时创建不同的引用,也有可能导致性能问题,我们可以利用 ES6 扩展运算符将传递的对象解构 。这样组件接收到的便是基本类型的 props,组件通过浅层比较发现接受的 prop 没有变化,则不会重新渲染。示例如下:

js 复制代码
// 不推荐
function Component(props) {
  const aProp = { someProp: 'someValue' };
  return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />  
}

// 推荐
const styles = { margin: 0 };

function Component(props) {
  const aProp = { someProp: 'someValue' };
  return <AnotherComponent style={styles} {...aProp} />  
}

通过解构属性,你实际上是将对象中的属性直接作为独立的基本类型属性传递给子组件,而不是整个对象。这样子组件接收到的是基本类型属性,React 可以使用浅层比较(shallow comparison)来判断属性是否发生变化。浅层比较只会比较属性值的引用,而不会深入比较属性内部的内容。

因此,当你解构属性传递给子组件时,如果对象的属性值没有变化,浅层比较会识别出属性没有变化,从而避免子组件的重新渲染。

9、使用 React.lazy() 和 Suspense 进行按需加载组件

React.lazy()SuspenseReact 提供的两个特性,用于实现组件的按需加载,从而优化应用的性能。这种方式允许你在需要的时候才加载组件,而不是在初始加载时将所有代码都打包在一起。

React.lazy():

React.lazy() 是一个函数,用于动态地导入一个组件。它允许你通过函数调用方式来按需加载组件,并且返回一个能够渲染该组件的动态导入组件。

基本语法:

jsx 复制代码
const MyComponent = React.lazy(() => import('./MyComponent'));

Suspense:

Suspense 是一个组件,它可以包裹在需要异步加载的组件外部,以指定在加载组件时如何显示加载状态或备用内容。使用 Suspense 的时候我们可以延迟组件的渲染,直到实际需要渲染的条件出现时。

基本语法:

jsx 复制代码
import React, { Suspense } from 'react';

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
}

结合使用 React.lazy()Suspense,你可以轻松地实现按需加载组件:

  • 使用 React.lazy() 动态导入需要按需加载的组件。
  • 使用 Suspense 组件包裹这个动态导入的组件,并在 fallback 属性中定义加载状态时显示的内容。

当页面渲染时,如果 MyComponent 组件尚未加载完成,Suspense 将会显示 fallback 中定义的加载状态(例如 "Loading..."),直到组件加载完成后再渲染实际内容。

需要注意的是,React.lazy()Suspense 目前只支持默认导出的组件。如果你需要导入命名导出的组件,可以使用以下方式:

jsx 复制代码
const MyComponent = React.lazy(() => import('./MyComponent').then(module => ({ default: module.MyComponent })));

举例🌰:

js 复制代码
import AnotherComponent from './another-component';
// 延迟加载不是立即需要的组件
const MUITooltip = React.lazy(() => import('./tooltip'));

function Tooltip({ children, title }) {
  return (
    <React.Suspense fallback={children}>
      <MUITooltip title={title}>
        {children}
      </MUITooltip>
    </React.Suspense>
  );
}

function Component(props) {
  return (
    <Tooltip title={props.title}>
      <AnotherComponent />
    </Tooltip>
  )
}

10、使用 useContext() 共享数据

假设我们的组件嵌套是这样的:A -> B -> C。其中 C 需要拿到 A 的一个状态。B 虽然不需要用到 A 的任何状态,但为了让 C 拿到状态,所以也用 props 接收了这个,然后再传给 C。这样的话,A 更新状态时,B 也要进行不必要的重渲染。这时使用 useContext 或通过状态管理来解决。

useContextReact 提供的一个钩子,用于在函数组件中访问 React 的上下文(context)。上下文是一种在组件树中共享数据的机制,可以避免将数据通过 props 层层传递给深层子组件,从而简化数据共享和传递的过程。

基本用法:

jsx 复制代码
const value = useContext(MyContext);
  • MyContext 是通过 React.createContext 创建的上下文对象。
  • value 是上下文提供的值。

使用步骤:

  1. 创建一个上下文对象:使用 React.createContext 创建一个上下文对象。

    jsx 复制代码
    const MyContext = React.createContext();
  2. 在顶层组件中提供上下文值:将数据或值传递给上下文提供器,将其包裹在组件树的顶层。

    jsx 复制代码
    <MyContext.Provider value={/* value */}>
      <App />
    </MyContext.Provider>
  3. 在子组件中使用上下文值:使用 useContext 钩子来获取上下文值。

    jsx 复制代码
    function ChildComponent() {
      const value = useContext(MyContext);
      // 使用 value
    }

优势和用途:

  • 共享数据useContext 允许你在组件树中共享数据,而无需通过 props 传递给每个中间组件。
  • 避免 prop drilling :通过 useContext,你可以避免在组件层次结构中进行多层级的 props 传递,提高代码的可读性和可维护性。
  • 主题和用户设置:可以用于实现主题切换、用户设置等全局状态的共享。
  • 解耦组件useContext 可以使组件不再直接依赖于特定的数据源,从而增加了组件的可复用性和灵活性。

Context 是粗粒度的

React 提供的 Context 的粒度是粗粒度的。当 Context 的值变化时,用到该 Context 的组件就会更新。

有个问题,就是 我们提供的 Context 值通常都是一个对象,比如:

js 复制代码
import React, { useState } from 'react';

const App = () => {
  const [visible, setVisible] = useState(false);
  return (
    <EditorContext.Provider value={ visible, setVisible }>
      <Editor />
    </EditorContext.Provider>
  );
}

每当 Contextvalue 变化时,用到这个 Context 的组件都会被更新,即使你只是用这个 value 的其中一个属性,且它没有改变。因为 Context 是粗粒度的

所以你或许可以考虑在高一些层级的组件去获取 Context,然后通过 props 分别注入到用到 Context 的不同部分的组件中

Contextvalue 在必要时也要做缓存,以防止组件的无意义更新。

js 复制代码
import React, { useState, useMemo } from 'react';

const App = () => {
  const [visible, setVisible] = useState(false);
  const EditorContextVal = useMemo(() => ({ visible, setVisible }), [visible, setVisible]);
  return (
    <EditorContext.Provider value={EditorContextVal}>
      <Editor />
    </EditorContext.Provider>
  );
}

11、使用 useTransition() 降低渲染优先级

useTransitionReact 提供的一个钩子,用于在并发模式(Concurrent Mode)下处理 UI 过渡的一种方式。并发模式是一种 React 的特性,旨在提升应用的性能和用户体验。

React 18 中引入的 useTransition 钩子允许你在异步操作中实现平滑的 UI 过渡效果,而无需在加载期间完全阻塞用户界面。这对于加载资源、处理网络请求等情况特别有用,可以保持界面的响应性。

startTransition 告诉 React 一些状态更新具有较低的优先级,即每个其他状态更新或 UI 渲染触发器具有较高的优先级。

基本用法:

jsx 复制代码
const [isPending, startTransition] = useTransition()
  • isPending:一个布尔值,表示是否有过渡正在进行。
  • startTransition:一个函数,用于触发过渡。它接受一个回调函数作为参数,这个回调函数中的操作会被视为一个异步操作,可以触发 UI 过渡。

使用场景:

  • 异步操作:当需要在进行异步操作(例如数据加载、网络请求)时,能够在过渡期间保持界面响应性,避免界面卡顿。
  • 平滑过渡:当异步操作会导致界面元素的变化,而你希望在数据加载期间进行平滑的界面过渡效果。

示例:

jsx 复制代码
import { useTransition } from 'react';

 function App() {
  const [isPending, startTransition] = useTransition();
  const [filterTerm, setFilterTerm] = useState('');
  
  const filteredProducts = filterProducts(filterTerm);
  
  function updateFilterHandler(event) {
    startTransition(() => {
      setFilterTerm(event.target.value);
    });
  }
 
  return (
    <div id="app">
      <input type="text" onChange={updateFilterHandler} />
      {isPending && <p style={{color: 'white'}}>Updating list..</p>}
      <ProductList products={filteredProducts} />
    </div>
  );
}   

12、使用 useDefferdValue() 允许变量延时更新

useDefferdValueReact 提供的一个钩子,用于在并发模式(Concurrent Mode)下处理 UI 过渡的一种方式。通过 useDefferdValue 允许变量延时更新,同时接受一个可选的延迟更新的最大值。React 将尝试尽快更新延迟值,如果在给定的 timeoutMs 期限内未能完成,它将强制更新。

js 复制代码
const defferValue = useDeferredValue(value, { timeoutMs: 1000 });

useDefferdValue 能够很好的展现并发渲染时优先级调整的特性 ,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染,等待这个状态更新完毕之后再渲染。

useDeferredValue的作用和useTransition一致,都是用于在不阻塞UI的情况下更新状态。但是使用场景不同。useTransition是让你能够完全控制哪个更新操作应该以一个比较低的优先级被调度。但是,在某些情况下,可能无法访问实际的更新操作(例如,状态是从父组件上传下来的)。这时候,就可以使用useDeferredValue来代替。

举例🌰:

假设我们有一个包含从 019999 数字的数组。这些数字在用户界面上显示为一个列表。该用户界面还有一个文本框,允许我们过滤这些数字。例如,我可以通过在文本框中输入数字 99 来过滤掉以 99 开头的数字。

js 复制代码
import { useState, useMemo, useDeferredValue } from 'react';

const numbers = [...new Array(200000).keys()];

// 父组件
export default function App() {
  const [query, setQuery] = useState('');

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  return (
    <div>
      <input type="number" onChange={handleChange} value={query} />
      <List query={query} />
    </div>
  );
}

// 子组件
function List(props) {
  const { query } = props;
  const defQuery = useDeferredValue(query);

  const list = useMemo(
    () =>
      numbers.map((i, index) =>
        defQuery ? (
          i.toString().startsWith(defQuery) && <p key={index}>{i}</p>
        ) : (
          <p key={index}>{i}</p>
        ),
      ),
    [defQuery],
  );

  return <div>{list}</div>;
}

13、使用错误边界组件捕获异常

默认情况下,若一个组件在渲染期间 render 发生错误,会导致整个组件树全部被卸载(页面白屏),这当然不是我们期望的结果。部分组件的错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 ------ 错误边界

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的异常,我们可以针对这些异常进行打印、上报等处理,同时渲染出一个降级(备用) UI,而并不会渲染那些发生崩溃的子组件树。

白话就是,被错误边界包裹的组件,内部如果发生异常会被错误边界捕获到,那么这个组件就可以不被渲染,而是渲染一个错误信息或者是一个友好提示!避免发生整个应该崩溃现象。

实现代码:

  • componentDidCatch(): 捕获错误,在这儿可以打印出错误信息、也可以对错误信息进行上报。
  • static getDerivedStateFromError(): 捕获错误,返回一个对象,更新 state
js 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    // 发生错误则: 更新 state
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 捕获到错误: 可以打印或者上报错误
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>深感抱歉, 系统出现错误!! 开发小哥正在紧急维护中.... </h1>;
    }
    return this.props.children; 
  }
}

// 错误边界使用
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

注意事项:

  • 错误边界目前只在类组件中实现了,没有在 hooks 中实现,因为 Error Boundaries 的实现借助了 this.setState 可以传递 callback 的特性,useState 无法传入回调,所以无法完全对标。

  • 错误边界无法捕获以下四种场景中产生的错误,仅处理渲染子组件期间的同步错误。

    • 自身的错误
    • 异步的错误
    • 事件中的错误
    • 服务端渲染的错误
  • 错误边界只能在类组件中实现了,并不是指 Error BoundaryHooks 不生效,而是指 Error Boundary 无法以 Hooks 方式指定,但是对功能是没有影响!你依然可以使用错误边界组件包裹使用了 hooks 的组件。

相关推荐
纯爱掌门人16 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl20 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅23 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人31 分钟前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼34 分钟前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空38 分钟前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_43 分钟前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范
子兮曰1 小时前
深入理解滑块验证码:那些你不知道的防破解机制
前端·javascript·canvas