hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~
当我们在使用框架进行单页应用开发的时候,比如vue,react,abgular等。我们通常只需要关注于业务逻辑的开发即可,对于视图层是如何渲染的,框架的底层已经帮我们做好了,无需再对视图层的渲染进行额外的操作。
但是对于一些大型的复杂项目来说,业务逻辑非常繁重的时候,我们不可避免的需要进行项目优化。而对于浏览器来说消耗性能最为严重的部分就是渲染和绘制。对于react来说,以一个组件的范围而言,首先就是要着眼于state
和props
的减少执行次数。
不过在此之前,我们还需要了解一下react
整个大致的执行流程,不过本文的重点还是讲解关于渲染控制api的讲解,整个react
的执行流程只会关注一个一致的流程走向。
一. 绕不开的Fiber
在react
整个创建和更新流程中起到关键作用的就是Fiber
架构,那么到底什么是Fiber
呢? 它在react
中作为最小的执行单元,作为一个节点存在,在内部包含了一系列创建和更新所需要的信息。在HTML
中的一切都可以找到所代表的fiber
节点,比如标签,普通文本等等。许多个fiber
节点互相关联、嵌套,就组成了代表整个应用的fiber
树。
js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Fiber元素的静态属性相关
this.tag = tag;
this.key = key; // fiber的key
this.elementType = null;
this.type = null; // 对应的DOM元素的标签类型,div、p...
this.stateNode = null; // 实例,类组件场景下,是组件的类,HostComponent场景,是dom元素
// Fiber 链表相关
this.return = null; // 指向父级fiber
this.child = null; // 指向子fiber
this.sibling = null; // 同级兄弟fiber
this.index = 0;
this.ref = null; // ref相关
// Fiber更新相关
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // 存储update的链表
this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表
this.dependencies = null;
this.mode = mode;
// Effects
// flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改
this.flags = NoFlags;
this.nextEffect = null;
// effect链相关,也就是那些需要更新的fiber节点
this.firstEffect = null;
this.lastEffect = null;
this.lanes = NoLanes; // 该fiber中的优先级,它可以判断当前节点是否需要更新
this.childLanes = NoLanes;// 子树中的优先级,它可以判断当前节点的子树是否需要更新
/*
* 可以看成是workInProgress(或current)树中的和它一样的节点,
* */
this.alternate = null;
}
其实整个fiber
树是以链表的形式进行首尾连接的,比如说我们现在有如下jsx
代码:
jsx
function App() {
return (
<div>
hello
<p>瓜瓜</p>
</div>
)
}
它最终会会形成如下fiber
链表:
其中每一个 fiber
是通过 return
, child
,sibling
(同级) 三个属性建立起联系的。return
: 指向父级 Fiber 节点。 child
: 指向子 Fiber 节点。 sibling
(同级):指向兄弟 fiber
节点。
整个fiber
架构中还存在双缓存树的概念,在每个fiber
结构中的alternate
保存着与之对应的workInProgress
缓存树。 当进行视图更新的时候,会同时存在两棵fiber
树,一个current
树,是当前显示在页面上内容对应的fiber
树。另一个是workInProgress
树,它是依据current
树深度优先遍历构建出来的新的fiber
树,所有的更新最终都会体现在workInProgress
树上。当更新未完成的时候,页面上始终展示current
树对应的内容,当更新结束时(commit
阶段的最后),页面内容对应的fiber
树会由current
树切换到workInProgress
树,此时workInProgress
树即成为新的current
树。
如下图所示:
还有一个点需要注意一下几个概念,element
、fiber
、 DOM
、其实这是在不同的处理过程的不同形态。
element
是React
视图层在代码层面的表现,也就是的 jsx 语法,元素结构,都会被创建成element
对象的形式。上面保存了props
,children
等信息。DOM
是元素在浏览器上真正的dom元素,也就是用于在浏览器中绘制的html。fiber
可以说是是element
和真实 DOM 之间的交流桥梁,每一个类型element
都会有一个与之对应的fiber
类型,element
变化引起更新流程都是通过fiber
做一次调和改变,然后形成新的 DOM 做视图渲染。
调和指的是:新旧dom树进行对比的过程。
二. render
render
阶段实际上是在内存中构建一棵新的fiber
树(称为workInProgress
树),构建过程是依照现有fiber
树(current
树)从root
开始深度优先遍历再回溯到root
的过程,这个过程中每个fiber
节点都会经历两个阶段:beginWork
和completeWork
。
beginWork
是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,而completeWork
是向上归并的过程,如果有兄弟节点,会返回 sibling
(同级)兄弟,没有返回 return
父级,一直返回到 FiebrRoot
。 组件的状态计算、diff
的操作以及render
函数的执行,发生在beginWork
阶段,effect
链表的收集、被跳过的优先级的收集,发生在completeWork
阶段。构建workInProgress
树的过程中会有一个workInProgress
的指针记录下当前构建到哪个fiber
节点,这是React更新任务可恢复的重要原因之一。
期间还会有Scheduler
进行任务调度,以便高优先级的任务会被优先处理。 但是本文重点不在此,所以不会赘述,以后会写别的文章。
三. commit
在render
阶段结束后,会进入commi
t阶段,该阶段不可中断,主要是去依据workInProgress
树中有变化的那些节点(render
阶段的completeWork
过程收集到的effect
链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect
以及同步执行useLayoutEffect
。
commit
细分可以分为三个阶段:
Before mutation
阶段:执行 DOM 操作前
没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate
,会在这里执行。
mutation
阶段:执行 DOM 操作
对新增元素,更新元素,删除元素。进行真实的 DOM 操作。
layout
阶段:执行 DOM 操作后
DOM 已经更新完毕。
React
最粗略的执行流程就说完了,而对于渲染控制,就是在各个流程的执行中,控制 render
的方式,通常有两种方式:
- 在父组件直接阻断子组件的渲染,比如
memo
。 - 从组件自身来控制
render
,比如:PureComponent
,shouldComponentUpdate
。
四. PureComponent
当类组件选择了继承 PureComponent
,就会会对props
和 state
进行浅比较,从而跳过不必要的更新(减少 render
的次数),提高组件性能。
jsx
import { PureComponent } from "react";
class Index extends PureComponent<any, any> {
constructor(props: any) {
super(props);
this.state = {
data: {
num: 0,
},
};
}
render() {
const { data } = this.state;
return (
<>
<div>hello</div>
<div> 数字: {data.num}</div>
<button
onClick={() => {
const { data } = this.state;
data.number++;
this.setState({ data });
}}>
数字长大
</button>
</>
);
}
export default Index;
可以看到数字并没有增加,因为PureComponent
会比较两次的 data 对象,引用地址并没有被改变,所以数字并不会增加,视图也不会更改·。 浅比较只会比较基础数据类型,对于引用类型,因为浅比较两次 data 还是指向同一个内存空间,所以并不会进行更新。
如果想要视图更新也很简单,只需要改变一下赋值方式:
js
const { data } = this.state;
data.number++;
this.setState({ data: {...data} })
那么 PureComponent
内部是如何工作的呢?
当我们的类组件选择继承 PureComponent
组件的时候,原型链上挂载一个 isPureReactComponent
属性:
js
pureComponentPrototype.isPureReactComponent = true;
isPureReactComponent
这个属性在更新组件 updateClassInstance
方法中会被使用:
js
function checkShouldComponentUpdate(
workInProgress: Fiber,
ctor: any,
oldProps: any,
newProps: any,
oldState: any,
newState: any,
nextContext: any
){
// 省略其他代码...
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
}
}
可以看到,react在内部会判断组件的原型链中是否存在isPureReactComponent
属性,然后进行浅比较props
和state
。
shallowEqual
函数是如何进行浅比较的呢?
js
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (Object.is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!Object.is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
- 首先比较新旧
props
,state
是否相等,如果相等,则返回 true,不更新组件; - 接下来判断新旧
props
,state
是否为对象,如果不是对象或为null
的情况,则返回 false,更新组件; - 通过
Object.keys
转化为数组,判断长度是否相等,不相等则需要更新 - 然后将新旧
props
,state
,利用key的长度进行遍历,深层次的进行比对,有新增或减少,返回 false,更新组件;
所以继承PureComponent
其实就相当于在内部自动帮你实现了shouldComponentUpdated
。
五. shouldComponentUpdated
shouldComponentUpdate
是一个生命周期的方法,直接在组件内部使用:
jsx
class Index extends Component{
state={
a:0, b:0
}
shouldComponentUpdate(newProp,newState){
if(newState.a !== this.state.a){
return true
}
return false
}
render(){}
}
shouldComponentUpdate
其实就是手动实现了PureComponent
的功能,两者也可以嵌套使用,在checkShouldComponentUpdate
函数中也有相对应shouldComponentUpdate
逻辑的处理。
js
function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
const instance = workInProgress.stateNode;
// 判断是否存在shouldComponentUpdate?
if (typeof instance.shouldComponentUpdate === 'function') {
let shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
从代码里面也可以看到当shouldComponentUpdate
和PureComponent
两者同时存在时,shouldComponentUpdate
优先级更高。
六. memo
可以对比 props
变化,来决定是否渲染组件,首先先来看一下 memo 的基本用法。 memo
接受两个参数,第一个参数 Component
原始组件本身,第二个参数 fn
是一个函数,可以根据一次更新中 props
是否相同决定原始组件是否重新渲染。
js
memo(Component,fn)
memo
第二个参数 返回true
组件不渲染 , 返回false
组件重新渲染。memo
当二个参数 fn 不存在时,会用浅比较 处理props
,相当于只比较props
版本的pureComponent
。
jsx
import { Component, memo } from "react";
const Child = ({ num, msg = "" }) => {
return (
<>
{console.log(`${msg}子组件渲染`)}
<p>
{msg}数字:{num}
</p>
</>
);
};
const MemoChild = memo(Child, (pre, next) => {
if (pre.num === next.num) return true;
if (next.num > 7) return false;
return true;
});
class Index extends Component {
constructor(props) {
super(props);
this.state = {
flag: true,
num: 1,
};
}
render() {
const { flag, num } = this.state;
return (
<div>
<Child num={num} />
<MemoChild num={num} msg="memo" />
<button onClick={() => this.setState({ flag: !flag })}>
状态切换{JSON.stringify(flag)}
</button>
<button
style={{ marginLeft: 8 }}
onClick={() => this.setState({ num: num + 1 })}
>
数字加一:{num}
</button>
</div>
);
}
}
export default Index;
当我们变更无关变量 flag 时,没有被 memo 包裹的子组件 Child
也会进行渲染,而包裹的则不会。同时 memo 的第二个参数可以主动控制是否渲染。
被 memo
包裹的组件,element
会被打成 REACT_MEMO_TYPE
类型的 element
标签,在 element
变成 fiber
的时候, fiber
会被标记成 MemoComponent
的类型。
js
function memo(type,compare){
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare, //第二个参数
};
return elementType
}
fiberTag = MemoComponent;
而React 对 MemoComponent
类型的 fiber 有单独的函数 updateMemoComponent
进行处理。
js
function updateMemoComponent(){
if (updateExpirationTime < renderExpirationTime) {
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual //如果 memo 有第二个参数,则用二个参数判定,没有则浅比较props是否相等。
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
}
}
// 返回将要更新组件,memo包装的组件对应的fiber,继续向下调和更新。
}
七. useMemo & useCallback
useMemo
useMemo
:理念与 memo
相同,都是判断是否满足当前的条件来决定是否执行callback
函数。在依赖不变的情况下,会返回相同的引用,避免子组件进行无意义的重复渲染。
返回值:更新之后的数据源,即 fn 函数的返回值,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的缓存值。
js
const cacheData = useMemo(fn, deps)
js
import { useState, useMemo } from "react";
const usePow2 = (list) => {
return list.map((item) => {
console.log("我是usePow2");
return item * 2;
});
};
// 被useMemo包裹
const usePow = (list) => {
return useMemo(
() =>
list.map((item) => {
console.log(1);
return Math.pow(item, 2);
}),
[],
);
};
const Index = () => {
let [flag, setFlag] = useState(true);
const data = usePow([1, 2, 3]);
const data2 = usePow2([1, 2, 3]);
return (
<>
<div>数字集合:{JSON.stringify(data)}</div>
<div>数字集合2:{JSON.stringify(data2)}</div>
<button onClick={() => setFlag((v) => !v)}>
状态切换{JSON.stringify(flag)}
</button>
</>
);
};
export default Index;
可以看到,即使传入的参数没有改变,未被useMemo
包裹的函数依然会执行。
useCallback
与 useMemo
用法一致,唯一不同的点在于,useMemo
返回的是值,而 useCallback
返回的是函数。
js
const res = useCallback(fn, deps)
返回值:即 fn 函数,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的函数。
用法:
js
const Index = () => {
let [count, setCount] = useState(0);
let [flag, setFlag] = useState(true);
const add = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<>
<button onClick={() => setCount((v) => v + 1)}>普通点击</button>
<button onClick={add}>useCallback点击</button>
<div>数字:{count}</div>
<button onClick={() => setFlag((v) => !v)}>
切换{JSON.stringify(flag)}
</button>
</>
);
};
useMemo
和useCallback
内部的实现非常相似:
初始化:
js
// mountMemo
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
js
// mountCallback
function mountCallback<T>(
callback: T,
deps: Array<mixed> | void | null
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
在初始化中,useMemo
首先创建一个 hook
,然后判断 deps
的类型,执行 nextCreate
,这个参数是需要缓存的值,然后将值与 deps 保存到 memoizedState 上。
而 useCallback
直接将 callback和 deps 存入到 memoizedState 里。
更新:
js
// updateMemo
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
// 判断新值
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
//之前保存的值
const prevDeps: Array<mixed> | null = prevState[1];
// 与useEffect判断deps一致
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
useMemo
通过判断两次的 deps 是否发生改变,如果发生改变,则重新执行 nextCreate()
,将得到的新值重新复制给 memoizedState
保存;如果没发生改变,则直接返回缓存的值。
js
// updateCallback
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
//之前保存的值
const prevDeps: Array<mixed> | null = prevState[1];
// 与useEffect判断deps一致
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看到useMemo
只是直接将依赖函数执行之后进行保存,而updateCallback
直接将依赖函数保存。
值得注意的是:虽然react提供了很多可以进行手动来控制渲染的方法,但是我们在大多数情况下不需要去特别的追求完美的控制每一次多余的更新和渲染。而且如果使用不慎,甚至会适得其反。如果在业务中封装业务组件或者需要一次性展示大量的数据等等情况下可以考虑手动进行优化。
至于后续react是如何根据生成的fiber
信息进行更新的,且听下回分解。
写在最后 ⛳
未来可能会更新react
和工程化相关的系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳
本文参考: juejin.cn/book/723062...