React源码核心系列之状态更新原理🤪🤪🤪
在 ReactDOM.createRoot(...)之后都经历了些什么? 迷迷糊糊的讲解了 JSX
和 Fiber
树的结构,那么这篇文章我们将来详细讲解一下整个 React
生命周期的运行流程。
什么是Fiber
在 Fiber
出现之前,react
会递归所有的 vdom
节点,如果 DOM
节点过多,会导致其他事件影响滞后例如点击事件,输入事件等等,造成了页面卡顿。
为了解决这一问题,react
引入了 fiber
这种数据结构,将更新渲染耗时长的大任务,分为许多的小片。每个小片的任务执行完成后,都先去执行其他高优先级的任务,并且能做到中断渲染和恢复。
这样 JavaScript
的主线程就不会被 react
独占,虽然任务执行的总时间不变,但是页面能够及时响应高优先级任务,显得不会卡顿了。
fiber
分片模式下,浏览器主线程能够定期被释放,去执行一些动画之类的,保证了渲染的帧率,为我们提供了一种跟踪、调度、暂停和中止工作的便捷方式,保证了页面的性能和流畅度。
fiber数据结构
在 React
中,每一个 React
元素对应一个 fiber
对象,那么每个 Fiber
对象有如下的属性,具体如下代码所示:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;// 对应组件的类型
this.key = key;// key属性
this.elementType = null;// 元素类型
this.type = null;// 函数组件或者类组件
this.stateNode = null;// 真实 DOM 节点
// Fiber
this.return = null;// 指向父节点
this.child = null;// 指向子节点
this.sibling = null;// 指向兄弟节点
this.index = 0;// 如果没有兄弟节点为0,如果是父节点下的子节点是数组,结合key做diff
this.ref = null;// ref 实例
this.pendingProps = pendingProps;// 新的props
this.memoizedProps = null;// 旧的props
this.updateQueue = null;// fiber上的更新队列
this.memoizedState = null;// 对应memoizedProps,上次渲染的state
this.dependencies = null;
this.mode = mode;// 表示当前组件下的子组件的渲染方式
// Effects
this.flags = NoFlags; // 用于记录fiber的状态(删除、新增、替换等)
this.subtreeFlags = NoFlags;// 当前子节点的副作用状态
this.deletions = null;// 删除的子节点的fiber
this.lanes = NoLanes; // 优先级
this.childLanes = NoLanes;
this.alternate = null;// current树和workInprogress树之间的相互引用
}
updateQueue
在上面的属性当中,我们看到了 this.updateQueue = null;
,它是 fiber
对象上的更新队列,每次更新都会创建一个 Update
对象,这个 Update
对象的结果如下所示:
export type Update<State> = {|
eventTime: number,
lane: Lane,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
|};
从上面的数据结构可以简单了解到,Update
是用来描述当前的对象操作的一些基本信息:时间(eventTime
)、优先级(lane
)、回调函数(callback
)、组件的类型(tag
)以及下一个更新对象(next
)。
initializeUpdateQueue
当调用 createFiberRoot(...)
函数创建 fiber
的时候,会在该函数内部自动调用 initializeUpdateQueue
函数,该函数的主要作用是为 fiber.updateQueue
属性上添加一个对象,具体代码如下所示:
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes,
},
effects: null,
};
fiber.updateQueue = queue;
}
enqueueUpdate
在上面的的这个命名当中是一个队列,但它并不是一个队列,而是一个链表结构,pending
永远指向最后一个 update
更新对象,第一个进入的 update
对象是排在第一个,最后一个 update
更新对象的 next
指针指向第一个,具体插入步骤如下图所示:
前者是刚开始只有一个更新时,当有新的更新插入时,会像后者一样,以此类推,形成一个单循环链表,具体代码如下所示:
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
): FiberRoot | null {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// fiber 被卸载时
return null;
}
// 返回一个对象 {interleaved:null, lanes:0, pending:null}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// pending 永远指向最后一个更新
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
const pending = sharedQueue.pending;
if (pending === null) {
// 第一次更新创建一个循环单链表
update.next = update;
} else {
// 如果更新队列不为空,取出第一个更新
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
}
enqueueUpdate调用
当我们首次渲染的时候,或者通过调用 setState
方法,都会调用 classComponentUpdater
对象下的 enqueueSetState(...)
方法,在该方法里面调用了 enqueueUpdate(fiber, update, lane)
,接下来我们通过控制台打印输出看看这个 update
的值是怎么样变化的,首先我们编写这样的页面组件:
import React, { Component } from "react";
class App extends Component {
constructor() {
super();
this.state = {
count: 1,
};
}
render() {
return (
<div>
<h2>count的值为 {this.state.count}</h2>
<h1
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 2 });
this.setState({ count: this.state.count + 3 });
}}
>
点击按钮
</h1>
</div>
);
}
}
export default App;
当首次渲染时,通过控制台打印 update
对象有如下所示:
首次渲染的值我们看完了,那么我们再看看通过 setState
后的 update
对象的是怎么改变的吧:
在 React18
之后,为了提高性能,避免每次调用 setState
都进行一次更新,避免 render
函数会被频繁调用,所以它采用的方法是批量更新,而 Update
做成单链表的主要原因也是这样,只需利用一次指针的查找便能查找到最后一个 update
对象,同时插入新的 update
对象也非常方便。
UpdateQueue 如何更新
当调度任务依次执行时,会调用 processUpdateQueue
函数计算最终的 state
,并且会根据 lane
来区分执行的时候有先后顺序,对应的是 processUpdateQueue
的先后执行顺序。
processUpdateQueue
- 首先保存了链表的第一个节点和最后一个节点,它们分别是
firstBaseUpdate
和lastBaseUpdate
; - 保存最后一次的更新
queue.shared.pending
赋值为变量pendingQueue
; - 如果
pendingQueue
不为空,记录第一个和最后一个update
,并将单循环链表设置为单链表; - 将待更新的第一个
update
对象添加到更新队列中的第一个更新元素,将最后一个添加到更新队列中的最后一个更新元素; - 判断
workInProgress.alternate
是否为空; - 获取当前的
currentQueue
,在React
的更新流程中,如果是ClassComponent
或者HostRoot
,currentQueue
原理永远不为空:- 首次渲染情况下:
2. setState情况下:
- 获取到当前队列中的最后一个更新
currentLastBaseUpdate
,如果它不等于第一个并且不等于空,则又将其设置为循环链表; - 如果
firstBaseUpdate
不为空,则将queue.baseState
赋值给newState
,例如通过点击按钮后,会保存上一次的状态{count: 1}
; - 进入
do...while(...)
循环; - 通过
isSubsetOfLanes
函数用于判断当前更新的优先级(updateLane
)是否满足更新队列的优先级; - 如果优先级不足则跳过,并将其存放在
newFirstBaseUpdate
和newLastBaseUpdate
中,会将处理好的newState
存放于newBaseState
中,更新并升级update
对象的优先级,让其能在下一次遍历时能够执行; - 如果优先级足够并且判断
newLastBaseUpdate
不为空,如果不为空说明之前有被跳过的uodate
对象,则将现在这个upodate
对象放入到newLastBaseUpdate
中并将被推迟的update
对象的优先级设置为Nolane
等级; - 调用
getStateFromUpdate
计算新的state
,并将其赋值在newState
中; - 如果调用
setState
时候传入有回调函数,那么会标记当前fiber
有callback
并存放在flags
中; - 继续遍历下一个
update
对象,检查queue.shared.pending
是否有更新,然后把剩余的推入到lastBaseUpdate
中,当pendingQueue
执行完成后退出循环; - 如果
newLastBaseUpdate
为空,说明没有被延迟的update
,会把整个队列的计算结构的newState
赋值给queue.baseState
; - 更新
workInProgress
节点的优先级,更新策略为如果没有优先级被跳过,则意味着本次将update
都处理完了,lanes
清空,否则将低优先级update
的优先级升级; - 更新
workInProgress
节点上的memoizedState
;
processUpdateQueue
函数的具体代码如下所示:
export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes
): void {
// This is always non-null on a ClassComponent or HostRoot
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
hasForceUpdate = false;
let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// 如果队列不为空,则把 fiber 中的 queue pending 指针置位空
queue.shared.pending = null;
// 获取最后一个更新
const lastPendingUpdate = pendingQueue;
// 因为是循环链表,获取 lastPendingUpdate.next 就是头节点
const firstPendingUpdate = lastPendingUpdate.next;
// 将其设置为单链表
lastPendingUpdate.next = null;
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
const current = workInProgress.alternate;
if (current !== null) {
const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
if (currentLastBaseUpdate !== lastBaseUpdate) {
if (currentLastBaseUpdate === null) {
currentQueue.firstBaseUpdate = firstPendingUpdate;
} else {
currentLastBaseUpdate.next = firstPendingUpdate;
}
currentQueue.lastBaseUpdate = lastPendingUpdate;
}
}
}
// These values may change as we process the queue.
if (firstBaseUpdate !== null) {
// 创建副本变量
let newState = queue.baseState;
console.log(newState);
let newLanes = NoLanes;
let newBaseState = null;
let newFirstBaseUpdate = null;
let newLastBaseUpdate = null;
let update = firstBaseUpdate;
do {
// 获取 update 的优先级,判断是否需要执行更新
const updateLane = update.lane;
const updateEventTime = update.eventTime;
// 更新队列的第一个 update 的优先级低于 renderLanes
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 由于优先级低于 renderLanes,不执行该 update 的更新
const clone: Update<State> = {
eventTime: updateEventTime,
lane: updateLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
// 本次没有更新的 update,会优先放到下一次去判断要不要更新
if (newLastBaseUpdate === null) {
newFirstBaseUpdate = newLastBaseUpdate = clone;
newBaseState = newState;
} else {
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// 更新 update 的 lane,往里塞入了满足条件的优先级,这样下次遍历到时才能执行
newLanes = mergeLanes(newLanes, updateLane);
} else {
if (newLastBaseUpdate !== null) {
const clone: Update<State> = {
eventTime: updateEventTime,
lane: NoLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance
);
const callback = update.callback;
if (
callback !== null &&
update.lane !== NoLane
) {
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
}
update = update.next;
if (update === null) {
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
break;
} else {
const lastPendingUpdate = pendingQueue;
const firstPendingUpdate =
((lastPendingUpdate.next: any): Update<State>);
lastPendingUpdate.next = null;
update = firstPendingUpdate;
queue.lastBaseUpdate = lastPendingUpdate;
queue.shared.pending = null;
}
}
} while (true);
if (newLastBaseUpdate === null) {
newBaseState = newState;
}
queue.baseState = ((newBaseState: any): State);
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
const lastInterleaved = queue.shared.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
newLanes = mergeLanes(newLanes, interleaved.lane);
interleaved = ((interleaved: any).next: Update<State>);
} while (interleaved !== lastInterleaved);
} else if (firstBaseUpdate === null) {
queue.shared.lanes = NoLanes;
}
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
}
}
getStateFromUpdate
在上面的函数中有调用过 getStateFromUpdate(...)
函数,在开始之前我们先通过控制台打印一下这些参数传进来的值分别是什么?
下面的图片中显示的是首次渲染的输出结果
下面的图片中显示的是调用 setState
的输出结果
prevState
存放的是上一次更新的值,而 update
对象是本次更新的信息。
在这个函数里面主要有 ReplaceState
、CaptureUpdate
、UpdateState
、ForceUpdate
四种类型更新,分别可以翻译为替换更新、捕获性更新、setState更新、强制性更新。
下面来对这四个更新进行讲解,它们分别是:
UpdateState
: 判断partialState
是否函数,如果是函数则直接调用后获取返回结果(你也可以将其理解为将setState
从异步变为同步),否则直接把partialState
作为newState
的一部分,最后与原来的state
使用assign({}, prevState, partialState)
进行合并;ReplaceState
: 与UpdateState
的流程相似,但是最后不会做合并,而是直接把结果作为新的state
返回;
参考文章
总结
从下载 React
源码到现在,过了大概半个月时间,满脑子天天都是 React
源码,现在可以放一放咯,准备春招实习去了。
不妨大胆一点,去做你想做的事情,成为你想称为的人🤗🤗🤗
转载自:https://juejin.cn/post/7209272192851656762