🔥🔥 React渲染你了解多少?

215 阅读9分钟

大纲

React在什么情况下渲染

什么是渲染

渲染是 React 将组件的状态和属性(props)转换为可视化表示(用户界面)的过程。

关键概念:

  • 组件:React 应用程序是由组件构成的,这些组件是可重用的 UI 部分,可以管理自己的状态并接收属性(props)。
  • 属性(Props) :这些是传递给组件的输入,允许数据从父组件传递到子组件。
  • 状态(State) :指的是组件可以管理的内部数据,这些数据可以随时间变化(通常响应用户的交互)。

渲染过程

React 从组件树的根节点开始,逐层查找所有需要更新的组件。对于每个被标记的组件,React 会调用函数(对于函数组件)或 render 方法(对于类组件),以渲染出屏幕内容。

触发渲染的方式

函数式

useState

下面来看一个例子

import { useState } from "react";

export default () => {
  const [count, setCount] = useState(0);
  return (
    <>
      useState
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        改变count
      </button>
    </>
  );
};

这个例子比较简单,点击count触发当前组件渲染。

那么问题来了,如果我在里面嵌套了一个子组件呢??子组件会刷新吗?

import { useState } from "react";

function Son() {
  console.log("触发刷新");
  return <h1>我是子组件</h1>;
}

export default () => {
  const [count, setCount] = useState(0);
  return (
    <>
      useState
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        改变count
      </button>
      <Son />
    </>
  );
};

初始化的时候触发了一次刷新,这是正常的

那么当我点击count,子组件会触发刷新吗?

答案是触发刷新了!!!,即使他没有依赖props,也触发了刷新,触发刷新的原因我们后面再说。现在只需要明白一件事情,setState会触发组件本身和子组件进行刷新。

useReducer

这个其实和useState一样,在React实现层面,useState是对useReducer的一层简单封装。

export default () => {
  const [count, forceRender] = useReducer((c) => c + 1, 0);
  return (
    <>
      useReducer
      {count}
      <button
        onClick={() => {
          forceRender(count + 1);
        }}
      >
        改变count
      </button>
      <Son />
    </>
  );
};

效果是一样的。

render(<App>)

可以再次调用render方法触发更新。 这个也是可以的。

类式组件

  • this.setState()
  • this.forceUpdate()

总结

上述内容展示了函数组件和类组件触发更新的方式。需要注意的是,当当前组件更新时,无论子组件是否有 props,子组件都会被触发重新渲染。这意味着即使子组件没有直接依赖于父组件的状态或属性,它仍然会随着父组件的更新而更新。这是 React 的设计理念,以保持 UI 的一致性。

阻止React重复渲染

在大多数情况下,我们并不需要阻止React重复渲染,因为React的理念就是纯函数,即使多渲染几次,展示到页面上的内容也是一致的。但是,当某个组件渲染及其珍贵(渲染一次需要很多时间),那就必须阻止重复渲染了!

下面会依次介绍可以阻止React重复渲染的方法

memo

React.memo 是一个高阶组件,用于优化函数组件的渲染性能。它可以通过对比 props 的变化来决定是否需要重新渲染组件。

function Son() {
  console.log("触发刷新");
  return <h1>我是子组件</h1>;
}
const MemoSon = memo(Son);

将son用memo包裹了一层,直接看运行结果

触发更新之后,看控制台只有一行打印,说明MemoSon没有重复渲染!

原理:

下面简单说下原理,后续会在源码层面进行分析他为什么会阻止重复渲染(点个关注,才能收到后续更新消息哦)

  • 浅比较:当使用 React.memo 包裹一个组件时,React 会在每次渲染时对该组件的 props 进行浅比较(Object.is)。如果 props 没有发生变化,则 React 会跳过该组件的渲染,直接复用上一次的输出。
  • 默认行为:如果组件的 props 没有变化(即未更新),React 将不会调用该组件的 render 方法。这样可以减少不必要的渲染,提高性能。

如果默认的浅比较不适用,可以传递第二个参数给 React.memo,以自定义比较逻辑 。

特殊情况

考虑以下情况,Son 组件接收一个 click 函数:

function Son({ click }) {
  console.log("触发刷新");
  return <h1>我是子组件</h1>;
}
const MemoSon = memo(Son);
export default ({ children }) => {
  const [count, forceRender] = useReducer((c) => c + 1, 0);
  const click = () => {};
  return (
    <>
      useReducer
      {count}
      <button
        onClick={() => {
          forceRender(count + 1);
        }}
      >
        改变count
      </button>
      <Son key={1} />
      <MemoSon click={click} />
      {children}
    </>
  );
};

son组件接收了一个click函数,那么问题来了,这种情况下会触发渲染吗?

在这个例子中,即使 count 没有变化,点击按钮时 MemoSon 组件仍会触发渲染。这是因为 click 函数在每次渲染时都会重新生成,因此 MemoSon 的 props 发生了变化,导致它重新渲染。

那么有什么解决方式?

  • 使用useCallback/useMemo
....
const newClick = useCallback(click, []);
      <MemoSon click={newClick} />
...

被useCallback包裹的函数会被缓存,所以只会触发一次更新。

  • 自定义更新函数
const MemoSon = memo(Son, () => {
  return true;
});
export default ({ children }) => {
  const [count, forceRender] = useReducer((c) => c + 1, 0);
  const click = () => {};
  const newClick = useCallback(click, []);
  return (
    <>
      useReducer
      {count}
      <button
        onClick={() => {
          forceRender(count + 1);
        }}
      >
        改变count
      </button>
      <Son key={1} />
      <MemoSon click={click} />
      {children}
    </>
  );
};

memo有第二个参数,当提供了第二个参数后,可以控制渲染时机。

注意

React.memo 的优化效果在于避免不必要的重新渲染,从而提高组件性能。然而,正确使用 memo 的前提是满足以下几个条件:

  • 频繁渲染的组件:当组件频繁渲染且传入相同的 props 时,使用 memo 可以显著提高性能。
  • 重渲染逻辑复杂的组件:如果组件的渲染逻辑复杂,且执行时消耗的性能较高,使用 memo 可以减少这种开销。

没必要使用memo的场景

  • 无明显延迟的渲染:如果组件的重新渲染速度很快,用户几乎感觉不到延迟,那么使用 memo 可能是多余的。甚至还有可能影响性能!
  • 传入的 props 总是不同:如果每次渲染时传入的 props 都不同(例如,每次都传入新的对象或函数),那么 memo 将失去作用。React 会在每次渲染时重新比较 props,因此会导致组件重新渲染。

为了解决 props 总是不同的问题,通常需要结合使用 useMemouseCallback

  • useCallback:用于缓存函数,确保在组件重新渲染时,如果依赖没有变化,就复用同一个函数实例。
javascript


复制代码
const handleClick = useCallback(() => {
  // 处理点击事件
}, [dependencies]); // 只有当 dependencies 变化时,handleClick 才会变化
  • useMemo:用于缓存计算值,避免在每次渲染时进行昂贵的计算。
javascript


复制代码
const memoizedValue = useMemo(() => {
  // 计算值
}, [dependencies]); // 只有当 dependencies 变化时,memoizedValue 才会重新计算
4. 总结
  • 使用 React.memo 的有效性依赖于组件的渲染频率和渲染复杂度。如果组件在每次渲染中都接收相同的 props,且渲染成本较高,memo 将会带来性能提升。
  • 当 props 总是不同或组件渲染迅速且无明显延迟时,memo 并没有必要。
  • 结合使用 useMemouseCallback 可以有效地缓存值和函数,避免不必要的重新渲染,从而充分发挥 memo 的优势。

children

export default ({ children }) => {
  const [count, forceRender] = useReducer((c) => c + 1, 0);
  return (
    <>
      useReducer
      {count}
      <button
        onClick={() => {
          forceRender(count + 1);
        }}
      >
        改变count
      </button>
      <Son key={1} />
      <MemoSon />
      {children}
    </>
  );
};

通过接受一个children,也可以解决重复渲染的问题。

    <DemoFn>
      <Son />
    </DemoFn>

直接来看结果

可以看到,是没有触发更新的。

React上下文

在 React 中,Context API 允许我们在组件树中共享数据,而无需通过每个级别的 props 逐层传递。Context 提供者(Provider)和消费者(Consumer)是实现这一点的核心。

用法

  1. 创建 Context: 首先,我们需要创建一个 Context 对象。这通常在组件的外部完成。

import React, { createContext } from 'react';

const MyContext = createContext();
  1. 使用 Context Provider: 接下来,我们使用 MyContext.Provider 将一个值传递给子组件。这个值将对所有嵌套的子组件可用。
const MyProvider = () => {
  return (
    <MyContext.Provider value={42}>
      <ChildComponent />
    </MyContext.Provider>
  );
};

在上面的示例中,MyProvider 组件将值 42 作为 value 属性传递给 MyContext.Provider

Context Consumer 的用法

子组件可以通过 MyContext.Consumer 来消费上下文。这种方式需要提供一个函数作为 render prop。


const ChildComponent = () => {
  return (
    <MyContext.Consumer>
      {value => (
      <h1>The value from context is: {value}</h1>
    )}
    </MyContext.Consumer>
  );
};

完整示例

下面是一个完整的示例,展示如何创建、提供和消费上下文:


import React, { createContext } from 'react';

// 创建上下文
const MyContext = createContext();

const ChildComponent = () => {
  return (
    <MyContext.Consumer>
      {value => (
      <h1>The value from context is: {value}</h1>
    )}
    </MyContext.Consumer>
  );
};

const MyProvider = () => {
  return (
    <MyContext.Provider value={42}>
      <ChildComponent />
    </MyContext.Provider>
  );
};

// 主应用组件
const App = () => {
  return (
    <div>
      <h1>Context API Example</h1>
      <MyProvider />
    </div>
  );
};

export default App;

渲染机制

稍微改造一下

import React, { createContext, useContext, useState } from "react";

// 创建上下文
const MyContext = createContext();

const ChildComponent = () => {
  console.log("ChildComponent触发刷新...");
  const { value } = useContext(MyContext);
  return <h1>{value}</h1>;
};
const ChildComponent2 = () => {
  console.log("ChildComponent2触发刷新...");
  return <h1>这是Child2</h1>;
};
const MyProvider = () => {
  const [count, setCount] = useState(42);

  return (
    <>
      <MyContext.Provider value={{ value: count, setValue: setCount }}>
        <ChildComponent />
        <ChildComponent2 />
      </MyContext.Provider>
      <button
        onClick={() => {
          setCount(100);
        }}
      >
        改变
      </button>
    </>
  );
};

export default MyProvider;

我们新增了一个子组件,同时有一个改变state的动作,看下结果如何。

可以看到,当Provider组件触发更新,父组件下面的所有子组件都会触发更新不管用没用到value

那我们如何避免这个行为呢,答案是用memo

import React, { createContext, memo, useContext, useState } from "react";

// 创建上下文
const MyContext = createContext();

const ChildComponent = () => {
  console.log("ChildComponent触发刷新...");
  const { value } = useContext(MyContext);
  return <h1>{value}</h1>;
};
const ChildComponent2 = () => {
  console.log("ChildComponent2触发刷新...");
  return <h1>这是Child2</h1>;
};

const Temp = memo(() => {
  return (
    <>
      <ChildComponent />
      <ChildComponent2 />
    </>
  );
});
const MyProvider = () => {
  const [count, setCount] = useState(42);

  return (
    <>
      <MyContext.Provider value={{ value: count, setValue: setCount }}>
        <Temp />
      </MyContext.Provider>
      <button
        onClick={() => {
          setCount(100);
        }}
      >
        改变
      </button>
    </>
  );
};

export default MyProvider;

我们多加一个Temp组件,Temp组件被memo包裹,现在我们看下行为吧。

当我们触发更新时,只有用到useContext的地方才会更新,其余地方不会。

所以上下文程序下的React组件应该用memo进行包裹,防止大量组件进行重复渲染。

这里其实有一个注意的地方是:当使用memo包裹组件时,useState触发更新,其memo组件如果不更新,其下的子组件是不会触发更新的而Context内部的子组件,如果memo组件本身没更新,但是子组件用到了Context的值,用到value的地方是会触发更新的,这是一个不同的地方。

总结

  1. 渲染逻辑:React 会在组件的 stateprops 变化时触发重新渲染,且子组件会随父组件更新。
  2. 优化策略
    • 使用 React.memo 优化频繁渲染的函数组件。
    • 使用 useCallbackuseMemo 缓存函数和计算值。
    • 使用 React.memo 避免 Context Provider 引发的所有子组件更新。
  1. 使用场景:若组件更新频率高且渲染代价大,建议使用优化手段提高性能;若没有明显延迟,不使用优化反而更简单。

通过合理使用 memouseCallbackuseMemo,以及优化 Context API 的性能,我们可以有效避免不必要的组件渲染,提高 React 应用的性能。