# React渲染机制
我们知道React初次渲染时会自上(通常是App.tsx)而下递归渲染整个组件树
而后续的更新渲染则和初次渲染不同,采用了局部更细机制
- 后续的更新渲染不会从根组件(如 App.tsx)开始,而是从触发状态或 props 变化的组件本身开始。具体来说:
如果某个组件的状态(通过 setState 或 useState)发生变化,React 会从该组件开始重新渲染。 如果父组件传递给子组件的 props 发生了变化,React 会对相应的子组件进行重新渲染。
- 虚拟dom比较(diff算法):
重新执行该组件的 render 方法(对于函数组件是重新执行函数体),生成新的虚拟 DOM 树。 React 会将新的虚拟 DOM 树与上一次渲染时保存的旧的虚拟 DOM 树进行比较,找出需要更新的部分。React 的 diff 算法只会比较同一层级的节点,不会全局进行比较。 根据 diff 结果,React 会计算出最小的 DOM 更新路径,避免不必要的重新渲染。
- 局部渲染和跳过不必要的更新:
在函数组件中,可以通过 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;
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;
2
3
4
5
6
7
8
9
当父组件改变state时,Child组件也重新渲染了
而在Vue3中,这种情况不会出现。vue3已经对该情况优化过了。
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
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>
</>
);
});
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结构的刷新来判断是否渲染👇
但是把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} />
</>
);
}
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
# 第二个参数
第二个参数是一个函数,返回true,不会触发render,返回false则会
propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean
# useMemo
useMemo() 基本用法如下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
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 />
</>
);
}
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>
</>
);
};
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}
</>
);
}
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 />
)
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>
);
}
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 />;
}
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名就是组件文件名