# React渲染机制

我们知道React初次渲染时会自上(通常是App.tsx)而下递归渲染整个组件树

而后续的更新渲染则和初次渲染不同,采用了局部更细机制

  1. 后续的更新渲染不会从根组件(如 App.tsx)开始,而是从触发状态或 props 变化的组件本身开始。具体来说:

如果某个组件的状态(通过 setState 或 useState)发生变化,React 会从该组件开始重新渲染。 如果父组件传递给子组件的 props 发生了变化,React 会对相应的子组件进行重新渲染。

  1. 虚拟dom比较(diff算法):

重新执行该组件的 render 方法(对于函数组件是重新执行函数体),生成新的虚拟 DOM 树。 React 会将新的虚拟 DOM 树与上一次渲染时保存的旧的虚拟 DOM 树进行比较,找出需要更新的部分。React 的 diff 算法只会比较同一层级的节点,不会全局进行比较。 根据 diff 结果,React 会计算出最小的 DOM 更新路径,避免不必要的重新渲染。

  1. 局部渲染和跳过不必要的更新:

在函数组件中,可以通过 React.memo 来优化组件。如果传递的 props 没有变化,React.memo 会阻止函数组件的重新渲染。 useMemo 和 useCallback:对于高开销的计算,可以使用 useMemo 缓存计算结果,而 useCallback 则可以缓存函数的引用,减少因函数重新定义导致的子组件更新。

# React组件缓存及渲染优化

  • react组件更新是自上而下的,即先父后子

  • 而vue组件更新,生命周期则是先子后父

参考:https://segmentfault.com/a/1190000025138329 (opens new window)

我们都知道当组件props和state发生改变时,当前组件以及其子孙组件会重新渲染,但是有一些组件(纯文本组件)是不需要重新渲染的,这种不需要的组件被重新渲染会影响整体的渲染性能。

React.memo()和PureComponent很相似,都是用来控制组件何时渲染的。

通过控制组件何时渲染可以帮助我们解决这个问题,在React中可以用来优化组件性能的方法大概有以下几种:

  • 组件懒加载(React.lazy(...)和<Suspense />)
  • PureComponent
  • shouldComponentUpdate(...){...}生命周期函数
  • React.memo
// Parent.tsx
import React, { useState } from 'react';
import Child from '../Child';

const Parent = () => {
 const [count, setCount] = useState(0);
 console.log('parent render update')
 return (
     <React.Fragment>
         <div>{count}</div>
         <button onClick={() => setCount(count + 1)}>++++</button>
         <Child msg='test'/>
     </React.Fragment>
 )
}

export default Parent;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Child.tsx
import React from 'react';

const Child = (props) => {
 console.log('child render update');
 return <div>{props.msg}</div>
}

export default Child;
1
2
3
4
5
6
7
8
9

当父组件改变state时,Child组件也重新渲染了

而在Vue3中,这种情况不会出现。vue3已经对该情况优化过了。

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});
1
2
3

# React.memo

React.memo是一个高阶组件 (opens new window)。功能与class component 中的PureComponent 类似

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

export default function Main() {
  const [number, setNumber] = useState(0);
  const [count, setCount] = useState(0);
  console.log("父组件 render");
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        btn
      </button>
      <BaseButton number={number} setNumber={setNumber} />
    </>
  );
}

const BaseButton = memo(({ number, setNumber }) => {
  console.log("----子组件重新渲染----");
  return (
    <>
      <button onClick={() => setNumber(number + 1)}>{number}</button>
    </>
  );
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

使用memo包裹子组件时,只有props发生改变子组件才会重新渲染,以提升一定的性能。

这里使用memo后BaseButton只有在number发生变化时,才会重新渲染。

可以通过观察dom结构的刷新来判断是否渲染👇

image-20230521213412278

但是把BaseButton放到Main父组件内后,每次父组件更新,子组件都会重新渲染,这种情况下就需要useMemo了

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

export default function Main() {
  const [number, setNumber] = useState(0);
  const [count, setCount] = useState(0);
  const BaseButton = memo(({ number, setNumber }) => {
    console.log("----子组件重新渲染----");
    return (
      <>
        <button onClick={() => setNumber(number + 1)}>{number}</button>
      </>
    );
  });
  console.log("父组件 render");
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        btn
      </button>
      <BaseButton number={number} setNumber={setNumber} />
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
image-20230521213820097

# 第二个参数

第二个参数是一个函数,返回true,不会触发render,返回false则会

propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean
1

# useMemo

useMemo() 基本用法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
1

useMemo() 返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)

memoized 值不变的情况下,不会重新触发渲染逻辑。

useMemo 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。

useMemo第二个参数

  • 空数组

    仅计算一次,即组件加载的时候

  • 不传

    If no array is provided, a new value will be computed on every render.

    如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-redner

首先是不使用任何优化方法

import React, { useState } from "react";

export default function Main() {
  const [number, setNumber] = useState(0);
  const handleClick = () => setNumber(number + 1);

  const BaseButton = () => {
    console.log("----子组件重新渲染----");
    return (
      <>
        <button onClick={() => setNumber(number+1)}>{number}</button>
      </>
    );
  };

  return (
    <>
      <span>{number}</span>
      <button onClick={handleClick}>btn</button>
      <BaseButton />
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这种情况,点击btn可以看到整个Main组件都会重新渲染。

而如果将BaseButton写在外面

import React, { useState } from "react";

export default function Main() {
  const [number, setNumber] = useState(0);
  const handleClick = () => setNumber(number + 1);

  return (
    <>
      <span>{number}</span>
      <button onClick={handleClick}>btn</button>
      <BaseButton mykey={"1312321"} name={"zs"} />
    </>
  );
}

const BaseButton = (props) => {
  console.log("----子组件重新渲染----", props);
  return (
    <>
      <button>BaseButton</button>
      <span>{props.mykey}</span>
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

虽然也会打印----子组件重新渲染----

但是dom是不会动的

但是可以看到只有span 自己渲染了

接着我们再使用useMemo

import React, { useMemo, useState } from "react";

export default function Main() {
  const [number, setNumber] = useState(0);

  const BaseButton = useMemo(() => {
    console.log("----子组件重新渲染----");
    return (
      <>
        <button onClick={() => setNumber(number + 1)}>{number}</button>
      </>
    );
  }, [number]);

  return (
    <>
      <span>{number}</span>
      <button onClick={() => setNumber(number + 1)}>btn</button>
      {BaseButton}
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

codesandbox (opens new window)

# useCallback

简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回值。

所以被useCallback包裹的组件,在使用时可以当成标签来使用

const Child = useCallback(()=> {
  //...
})
return (
  <Child />
)
1
2
3
4
5
6

useCallback会导致父组件re-render

# diff算法

react 16 之前的 diff:https://www.cnblogs.com/forcheng/p/13246874.html

Fiber: https://zhuanlan.zhihu.com/p/26027085

React 通过大胆的假设,并基于假设提出相关策略,成功的将 O(n^3) 复杂度的问题转化为 O(n) 复杂度的问题。

# diff优化

  • tree diff
  • Component diff
  • Element diff

# Tree diff

React 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。

# React组件懒加载

在 React 中异步加载组件可以采用两种方式:使用import()函数或者使用React.lazy

1. 使用React.lazy:

import { lazy, Suspense } from 'react';

const AsyncComponent = lazy(() => import('./AsyncComponent'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <AsyncComponent />
      </Suspense>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

需要注意的是,在使用React.lazy时,只能用在默认导出的组件上

2. 使用import()函数:

import { useState, useEffect } from 'react';

function App() {
  const [AsyncComponent, setAsyncComponent] = useState(null);

  useEffect(() => {
    // 异步加载组件
    import('./AsyncComponent').then((module) => {
      setAsyncComponent(module.default);
    });
  }, []);

  if (!AsyncComponent) {
    return <div>Loading...</div>;
  }

  return <AsyncComponent />;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

React16.6.0 版本开始,React 原生提供了 code-splitting 与组件动态加载的方案,引入了 lazy 函数,同时也支持使用 Suspense 与 ErrorBoundary 来进行异常处理与 fallback 展示。

所以我们在React中使用lazy和import()加载异步组件后,组件默认就是split过的,chunk名就是组件文件名