React18.2x源码解析:类组件的加载过程
在之前的章节我们讲述了FiberTree
的创建过程,但是对组件的加载过程这方面的细节没有深入。
本节将深入理解React18.2x 类组件的具体加载过程。
1,加载阶段
首先准备一个类组件案例:
import React, { Component } from 'react';
export default class MyClass extends Component {
constructor(props) {
super(props)
console.log('MyClass组件运行了')
this.state = {
count: 1
}
}
componentWillMount() {
console.log('MyClass组件WillMount完成')
}
componentDidMount() {
console.log('MyClass组件mount完成')
}
handleClick = () => {
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log('回调钩子1执行', this.state.count)
})
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log('回调钩子2执行', this.state.count)
})
}
// 组件的渲染
render() {
return (
<div className='MyClass'>
<div>MyClass组件</div>
<div>state: {this.state.count}</div>
<div>name: {this.props.name}</div>
<button onClick={this.handleClick}>更新</button>
</div>
);
}
}
然后直接进入到class
组件对应的Fiber
节点加载:
执行该Fiber
节点的beginWork
工作,根据tag
类型,进入class
组件的逻辑处理【case ClassComponent
】:
这里要注意:当Fiber
节点对应的是组件类型时,它的type
属性存储的就是我们定义的组件原型,类组件即为原始的class
定义,它会在类组件首次加载时被调用,创建一个对应的组件实例。
下面我们直接进入updateClassComponent
方法,查看class
组件的加载。
updateClassComponent
查看updateClassComponent
方法:
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
// 类组件节点的stateNode 不是存储dom元素,而是组件实例 【hostComponent的stateNode才是dom元素】
const instance = workInProgress.stateNode;
let shouldUpdate;
# 第一次class组件加载,instance都为null
if (instance === null) {
// 初始化构建组件实例
constructClassInstance(workInProgress, Component, nextProps);
// 加载组件实例
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
// update阶段:判断是否更新class组件
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
# 最后:创建class组件child
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
# 返回child子节点
return nextUnitOfWork;
}
updateClassComponent
方法的内容并不复杂,主要就是两个逻辑的执行:
class
组件实例的创建或者更新。- 创建组件的
child
子节点,最后返回子节点。
这里主要讲解第一点的内容,关于Fiber
节点的创建在《React18.2x源码解析(三)Reconciler协调流程》有已经完整的讲述。
const instance = workInProgress.stateNode;
首先从Fiber
节点的stateNode
属性取出组件实例,当前为类组件的初次加载,所以instance
肯定为null
。
关于
Fiber
节点的stateNode
属性:如果为类组件的节点,则该属性存储的为组件实例,如果为hostComponent
类型的节点,则存储的是真实的DOM结构。
满足第一个if
条件,进入内部代码执行:
# 首次加载
if (instance === null) {
// 初始化构建组件实例
constructClassInstance(workInProgress, Component, nextProps);
// 加载组件实例
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
}
这里主要是调用了两个方法:
constructClassInstance
:构建class
组件实例。mountClassInstance
:加载组件。
下面我们按执行顺序进行讲解。
constructClassInstance
查看constructClassInstance
方法:
// packages\react-reconciler\src\ReactFiberClassComponent.new.js
function constructClassInstance(
workInProgress: Fiber,
ctor: any,
props: any,
): any {
# 创建class实例对象,参数为props和context
let instance = new ctor(props, context);
// 将instance实例对象的state数据同步到Fiber节点的memoizedState属性
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);
# 确定class组件实例:即链接FiberNode与对应的组件实例
adoptClassInstance(workInProgress, instance);
// dev开发环境下的警告:如果class组件使用了过时的生命周期钩子,发出对应的警告
...
# 返回创建完成的实例
return instance;
}
首先直接通过new
关键调用我们定义的class
,创建一个实例对象Instance
:
let instance = new ctor(props, context);
这里创建class
的实例时,就会调用类的构造器函数,执行我们在constructor
中编写的代码:
constructor(props) {
super(props)
console.log('MyClass组件运行了')
this.state = {
count: 1
}
}
控制台打印出我们准备的日志:
然后使用实例对象中的state
数据更新Fiber
节点中的数据,因为在组件更新时,需要使用Fiber
节点上的数据参与计算:
// 更新数据
workInProgress.memoizedState = instance.state || null;
然后调用了一个adoptClassInstance
方法:它主要有两个作用:
- 确定类组件的更新器
updater
对象。 - 关联
Fiber
节与之对应的组件实例Instance
。
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
// FIber节点存储instance实例
workInProgress.stateNode = instance;
// instance对象定义一个_reactInternal内部属性存储Fiber节点
setInstance(instance, workInProgress);
}
更新器
updater
与setState
方法有关,我们在类组件中使用this.setState
时,实际上就是调用的updater
对象中的方法,更多的细节可以查看《React18.2x源码解析:React常用API原理》中Component
原理。
workInProgress.stateNode = instance;
这里将创建完成的组件实例存储到Fiber
节点的stateNode
属性,同时就可以印证前面的updateClassComponent
方法为何通过这个属性是否有值来判断当前类组件是加载阶段还是更新阶段。
adoptClassInstance
方法执行完成后,会有一些开发环境下的校验【代码较多已省略】:如果在class
组件使用了过时的生命周期钩子,则会发出相应的警告进行提示:
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillUpdate
最后返回创建完成的instance
实例对象,constructClassInstance
方法执行完成。
mountClassInstance
查看mountClassInstance
方法:
// packages\react-reconciler\src\ReactFiberClassComponent.new.js
function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): void {
// 取出class组件实例
const instance = workInProgress.stateNode;
// props
instance.props = newProps;
// 数据
instance.state = workInProgress.memoizedState;
// ref:默认为空对象
instance.refs = emptyRefsObject;
// 初始化一个Fiber节点的更新队列
// 设置更新队列对象:fiber.updateQueue = queue;
initializeUpdateQueue(workInProgress);
// 同步组件实例的state数据
instance.state = workInProgress.memoizedState;
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
// 调用getDerivedStateFromProps钩子
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
instance.state = workInProgress.memoizedState;
}
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
// 不应该使用旧的生命周期钩子
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
// 触发WillMount生命周期钩子
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
// 如果设置了class组件的componentDidMount生命周期钩子函数,则需要在组件的FiberNode上设置对应的flags
if (typeof instance.componentDidMount === 'function') {
let fiberFlags: Flags = Update;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStatic;
}
workInProgress.flags |= fiberFlags;
}
}
首先更新了instance
组件实例上的一些属性,然后初始化了当前组件Fiber
节点的updateQueue
属性:
initializeUpdateQueue(workInProgress);
export function initializeUpdateQueue<State>(fiber: Fiber): void {
// 创建一个更新队列对象
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState, // 初始state数据
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null, // 存储update链表
lanes: NoLanes,
},
effects: null,
};
// 设置updateQueue属性
fiber.updateQueue = queue;
}
关于updateQueue
属性的更多细节我们会在更新阶段详细讲解。
代码中出现了多次更新instance.state
属性,因为context
和props
都有可能引起Fiber
对象的数据变化,需要时刻与instance
的数据保持同步。
instance.state = workInProgress.memoizedState;
然后就是针对componentWillMount
生命周期函数钩子的处理,虽然是过时的API
,但是如果定义了还是需要在这里触发。
// 触发WillMount生命周期钩子
callComponentWillMount(workInProgress, instance);
此时控制台就会打印出对应的日志:
这些钩子函数中有可能会引起数据的变化,需要再次同步更新instance.state
属性。
最后如果定义了componentDidMount
钩子函数,则需要给该Fiber
节点的flags
属性设置对应的副作用标记。
if (typeof instance.componentDidMount === 'function') {
workInProgress.flags |= fiberFlags;
}
flags
标记的作用是在commit
阶段执行对应的副作用操作,所以componentDidMount
钩子函数会在commit
阶段中进行触发调用。
到此为止,类组件的加载过程就完成了。
2,更新阶段
点击案例的更新按钮,触发一次组件更新,进入类组件的更新阶段。
点击更新按钮,就会执行this.setState
方法,这里的enqueueSetState
函数其实就是this.setState
真正执行的内容。
# setState原理
Component.prototype.setState = function(partialState, callback) {
// 调用updater中的一个方法
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
enqueueSetState
查看enqueueSetState
方法:
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
const classComponentUpdater = {
isMounted,
# 这就是setState调用的方法
enqueueSetState(inst, payload, callback) {
// 取出instance实例对应的FiberNode节点
const fiber = getInstance(inst);
const eventTime = requestEventTime();
// 获取FIber对应的lane优先级
const lane = requestUpdateLane(fiber);
// 创建update更新对象
const update = createUpdate(eventTime, lane);
// 确定更新内容 { count: 1 }
update.payload = payload;
if (callback !== undefined && callback !== null) {
// 存储传入的cb回调函数
update.callback = callback;
}
# 将update更新对象添加到更新队列,并返回应用的root根节点
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
# 开启一个从root根节点开始的更新调度
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitions(root, fiber, lane);
}
},
enqueueReplaceState() {},
enqueueForceUpdate() {}
}
这里我们要重点关注一下类组件关于update
对象和updateQueue
属性的处理。
首先创建本次的update
更新对象【update1
】:
const update = createUpdate(eventTime, lane);
// 设置更新的内容
update.payload = payload;
// 设置回调函数
update.callback = callback;
// 第一次的setState定义
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log('回调钩子1执行', this.state.count)
})
这里的payload
属性存储的就是setState
的第一个参数,也就是修改数据的内容。
callback
属性存储的就是setState
的第二个参数,我们定义的回调函数。
所以这也就是为啥说update
对象存储的就是更新操作相关的信息。
下面我们接着再看updateQueue
属性的处理。
const root = enqueueUpdate(fiber, update, lane);
首先看当前类组件Fiber
节点的updateQueue
属性:
可以看出当前的updateQueue
对象中除了baseState
属性存储着更新前的state
数据之外,其他内容都是空的。
下面我们接着看enqueueUpdate
方法对updateQueue
属性的处理:
export function enqueueUpdate() {
const updateQueue = fiber.updateQueue;
const sharedQueue = updateQueue.shared;
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
取出了updateQueue
属性和shared
属性作为enqueueConcurrentClassUpdate
方法的参数传入:
接着查看enqueueConcurrentClassUpdate
方法:
function enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) {
const interleaved = sharedQueue.interleaved;
// 第一个update对象入队
if (interleaved === null) {
update.next = update;
pushConcurrentUpdateQueue(sharedQueue);
} else {
// 其他的update对象入队
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
}
首先取出sharedQueue
的interleaved
属性【即updateQueue.shared.interleaved
】,如果interleaved
为null
,表示为当前的update1
为第一个入队的更新对象,将此update1
的next
属性指向自身,形成一个单向环状链表。
然后调用了一个pushConcurrentUpdateQueue
方法,这个方法的作用是将sharedQueue
备份到一个并发队列concurrentQueues
之中,方便在之后将sharedQueue.interleaved
的内容转移到sharedQueue.pending
之上。
interleaved
只是一个临时存储update
链表的属性,最终会在更新之前转移到pending
属性之上用于计算。
pushConcurrentUpdateQueue(sharedQueue);
最后设置sharedQueue.interleaved
为当前的update
对象。
至此,第一个this.setState
操作的update1
入队处理完成。
回到enqueueSetState
方法中,这个方法最后会调用scheduleUpdateOnFiber
函数进入更新的调度程序。
关于调度任务的细节可以查看《React18.2x源码解析(二)scheduler调度程序》。
click
事件触发的更新任务为同步任务:下面直接快进,来到同步任务的处理:
这里首先会调用scheduleSyncCallback
方法,将处理同步任务的performSyncWorkOnRoot
回调函数添加到同步队列syncQueue
。
然后在支持微任务的环境下:就会使用scheduleMicrotask
方法,这个方法等同于Promise.then
:
Promise.then(flushSyncCallbacks)
这里就会将冲刷同步任务队列syncQueue
的flushSyncCallbacks
函数添加到微任务中,然后继续向下执行。
注意: 我们在DOM事件中执行了两次this.setState
:
handleClick = () => {
// 第一次
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log('回调钩子1执行', this.state.count)
})
// 第二次
this.setState((state, props) => {
return {
count: state.count + 1
}
}, () => {
console.log('回调钩子2执行', this.state.count)
})
}
上面第一次setState
执行完成,主要就是将第一个update
对象进行了入队处理,同时将冲刷同步任务队列的flushSyncCallbacks
函数添加到了微任务之中,等待异步处理。
但是我们的同步代码还没有执行完成,还有第二个setState
等待执行:
再次进入enqueueSetState
方法:
第二次调用setState
还会新建一个update
更新对象【update2
】,依然会执行入队操作。
function enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane) {
const interleaved = sharedQueue.interleaved;
// 第一个update对象入队
if (interleaved === null) {
...
} else {
// 其他的update对象入队
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
}
此时update2
非第一个入队的对象,所以就会进入else
分支处理:
- 将当前的
update2
对象的next
属性执行第一个update1
。 - 将第一个
update1
的next
属性指向当前的update2
对象。
最后将sharedQueue.interleaved
设置为最新的update2
。
至此,update2
也已经入队完成,此时shared.interleaved
指向的就是最新的update2
。
回到enqueueSetState
方法中,最后还是会调用scheduleUpdateOnFiber
函数进入更新的调度程序。
但是这次在调度时发现新的调度优先级和现存的优先级相同,可以归为同一个任务处理,就不会再重复调度。
最后触发return
关键字,结束本次同步代码的执行。
flushSyncCallbacks
来微任务队列,开始执行flushSyncCallbacks
方法:
可以看出syncQueue
同步任务队列之中就有一个任务,即performSyncWorkOnRoot
函数。
后面的逻辑就直接简单介绍了,方便快速进入到类组件的更新程序:
callback = callback(isSync);
循环syncQueue
队列,从中取出callback
回调函数,然后调用回调函数【performSyncWorkOnRoot
】。
直接进入到performSyncWorkOnRoot
方法中:
function performSyncWorkOnRoot(root) {
...
var exitStatus = renderRootSync(root, lanes);
}
调用renderRootSync
方法,开始FiberTree
的创建过程。
在这之前,还有一个处理要注意:
function renderRootSync() {
...
prepareFreshStack()
}
function prepareFreshStack() {
...
finishQueueingConcurrentUpdates()
}
在renderRootSync
中会调用一个prepareFreshStack
方法,这个方法主要是确定参数本次创建FiberTree
的hostFiber
根节点,但是这个方法最后调用了finishQueueingConcurrentUpdates
函数,这个函数作用就是循环并发队列concurrentQueues
,将之前存储的queue
对象的更新链表从share.interleaved
中转移到share.pending
中,代表此节点有等待处理的更新操作。
interleaved
属性主要是插入时存储,现在已经转移到pending
属性中:
下面我们直接快进到类组件的Fiber
节点处理:
进入beginWork
工作的classComponent
处理分支,开始类组件的更新:
继续查看updateClassComponent
方法:
更新阶段时:这里组件实例instance
和旧的Fiber
节点【current
】都存在,只能进入else
分支,开始类组件的更新执行。
updateClassInstance
进入updateClassInstance
方法:
// packages\react-reconciler\src\ReactFiberClassComponent.new.js
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): boolean {
//... 省略代码
}
updateClassInstance
方法里面的内容比较多,我们分成以下几个部分来讲解:
- 根据
updateQueue
计算state
。 - 调用
getDerivedStateFromProps
钩子。 - 调用
shouldComponentUpdate
钩子,检查组件是否应该更新。 - 为组件
Fiber
的flags
属性设置componentDidUpdate
和getSnapshotBeforeUpdate
副作用标记。
(一)根据updateQueue
计算state
。
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
ctor: any,
newProps: any,
renderLanes: Lanes,
): boolean {
// 取出组件实例
const instance = workInProgress.stateNode;
// 从之前的节点上克隆updateQueue信息,包含了shared.pending中等待处理的更新操作
cloneUpdateQueue(current, workInProgress);
// 旧的props
const unresolvedOldProps = workInProgress.memoizedProps;
const oldProps =
workInProgress.type === workInProgress.elementType
? unresolvedOldProps
: resolveDefaultProps(workInProgress.type, unresolvedOldProps);
instance.props = oldProps;
// 新的props
const unresolvedNewProps = workInProgress.pendingProps;
...
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
typeof getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function';
// 取出旧的数据
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
# 更新组件实例的数据
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;
...
}
首先处理之前的updateQueue
信息:
cloneUpdateQueue(current, workInProgress);
这里就是将current
节点中的updateQueue
信息克隆到workInProgress
节点中对应的属性中。
注意: react应用每次更新都会执行Fiber Reconciler
流程【即FiberTree
的创建流程】,在进入此流程之前,current
节点会存储本次更新相关的一些信息,但是在进入此流程之后,current
就变成了旧的节点,workInProgress
代表新建的节点,此时就需要将current
节点上的一些信息保存到新的节点之中,也就是对应的workInProgress
。
在这里更新workInProgress.updateQueue
属性信息,是为了下面通过计算后生成新的state
数据。
定义两个变量存储旧的props
和新的props
,方便后续使用:
// 旧的props
const unresolvedOldProps = workInProgress.memoizedProps;
// 新的props
const unresolvedNewProps = workInProgress.pendingProps;
从class
类中取出getDerivedStateFromProps
钩子,判断当前类组件有没有使用此钩子,如果没有使用则设置变量hasNewLifecycles
为false
,此变量的作用是后续判断其他生命周期钩子的执行与否。
下面我们开始进入state
的处理,这是类组件更新的重点逻辑:
首先取出旧的state
数据:
// memoizedState代表旧的数据,就像memoizedProps代表旧的props
const oldState = workInProgress.memoizedState;
重点来了:调用processUpdateQueue
方法,根据updateQueue
信息,计算生成新的state
数据。
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
计算state
进入processUpdateQueue
方法:
// packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js
export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes,
): void {
const queue = workInProgress.updateQueue;
let firstBaseUpdate = queue.firstBaseUpdate; // 第一个处理的update,一个完整指向的队列
let lastBaseUpdate = queue.lastBaseUpdate; // 最后一个处理的update
// Check if there are pending updates. If so, transfer them to the base queue.
// pending:等待处理的更新操作
// 如果pending存在内容,则重置它,将它的内容转移到基础队列 firstBaseUpdate
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// 重置pending:因为workInProgress和current同用一个shared对象,所以current的pengding也重置了
queue.shared.pending = null;
# 重构pendingQueue,将单向环状链表变成普通的单向链表
// 最后处理的update对象
const lastPendingUpdate = pendingQueue;
// 第一个处理的update对象
const firstPendingUpdate = lastPendingUpdate.next;
// 断开链接:最后一个update不再指向第一个update,形成一个单向链表,不再首尾相连
lastPendingUpdate.next = null;
// Append pending updates to base queue
// 将等待处理的更新对象添加到firstBaseUpdate基础更新队列
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
}
// These values may change as we process the queue.
// 当我们处理队列时,这些值可能会发生变化。
if (firstBaseUpdate !== null) {
// Iterate through the list of updates to compute the result.
let newState = queue.baseState;
let newLanes = NoLanes;
let newBaseState = null;
let newFirstBaseUpdate = null;
let newLastBaseUpdate = null;
// 第一个更新的update
let update = firstBaseUpdate;
# 循环update链表计算state
do {
// TODO: Don't need this field anymore
const updateEventTime = update.eventTime;
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
// 根据每个update对象的lane来判断是否更新:
// 检查此更新update是否是在隐藏树时进行的。如果是,组件在隐藏时不需要更新
// 跳过更新
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
// 不必更新【跳过更新】
...
} else {
// This update does have sufficient priority.
// 此更新具有足够的优先级【正常更新】
if (newLastBaseUpdate !== null) {
const clone: Update<State> = {
eventTime: updateEventTime,
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null,
};
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// Process this update.
// 处理当前update对象
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);
const callback = update.callback;
// If the update was already committed, we should not queue its
// callback again.
// 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
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 = update.next;
// update有值,开启下一次循环
if (update === null) {
// 如果为null,代表链表执行完成,退出循环,表示本次更新state计算完成
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
break;
} else {
// An update was scheduled from inside a reducer. Add the new
// pending updates to the end of the list and keep processing.
const lastPendingUpdate = pendingQueue;
// Intentionally unsound. Pending updates form a circular list, but we
// unravel them when transferring them to the base queue.
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;
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
// 更新state数据
workInProgress.memoizedState = newState;
}
}
虽然processUpdateQueue
方法里面的内容不少,但是其逻辑并不复杂,下面我们开始按代码顺序开始解析。
const queue = workInProgress.updateQueue;
首先用一个变量queue
存储当前类组件节点的updateQueue
属性值:
然后从queue
对象取出firstBaseUpdate
和lastBaseUpdate
两个属性值,并用两个变量来存储它们的值:
firstBaseUpdate
:表示第一个处理的update
对象。lastBaseUpdate
:表示最后一个处理的update
对象。
这两个值在正常更新情况下默认都是为null
的,只有在某个update
优先级较低【比如当前组件节点处于hidden
状态】,遗留到下次更新才会被保存到这两个值中,这种情况较少,对我们来说,只需要掌握正常更新的逻辑即可。
下面开始处理pendingQueue
:
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// 重置pending:因为workInProgress和current同用一个shared对象,所以current的pengding也重置了
queue.shared.pending = null;
// 最后处理的update对象
const lastPendingUpdate = pendingQueue;
// 第一个处理的update对象
const firstPendingUpdate = lastPendingUpdate.next;
// 断开链接:最后一个update不再指向第一个update,形成一个单向链表,不再首尾相连
lastPendingUpdate.next = null;
// Append pending updates to base queue
// 将等待处理的更新对象添加到基础队列
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
}
这里取出pending
的内容存储到pendingQueue
,代表本次计算state
相关的update
队列。
- 如果
pendingQueue
为null
,代表没有需要处理的update
队列,state
将不会得到更新。 - 如果
pendingQueue
不为null
,代表有需要处理的update
队列,开始计算更新state
。
当前我们的pendingQueue
是有值的,所以需要更新state
。这里重置pending
为null
【已经拿到内容,不再需要保留,重置后方便下次更新重新添加】。
然后设置lastBaseUpdate
为pendingQueue
,因为pendingQueue
自身就是指向最后一个update
对象。
这里我们在回顾一下之前的图例印证:
然后设置firstBaseUpdate
的值为lastBaseUpdate.next
,最后一个update
对象的next
属性指向第一个update
。
所以firstBaseUpdate
就是第一个处理的update
对象。
注意: 这里设置lastBaseUpdate
的next
属性为null
,不再指向第一个update
。
lastBaseUpdate.next = null;
至此,firstBaseUpdate
将变成一个普通的单向链表,而不是环状。
这是因为后面循环处理
update
更新队列时,会一直从next
属性取出下一个要处理的update
,直到为null
时结束循环。
if (firstBaseUpdate !== null) {
...
}
然后判断firstBaseUpdate
是否有值,有值则循环取出update
对象,开始计算更新state
。
// 更新之前的state
let newState = queue.baseState;
// 第一个处理的update
let update = firstBaseUpdate;
这里取出baseState
,它是更新之前的state
数据,也是代表参与本次计算的基础state
。
然后定义了一个update
变量,代表第一个处理的更新对象。
do {
...
} while (true);
处理update
对象的逻辑是一个do while
循环结构,它的判断条件永远为真,只有处理完所有update
对象才会跳出循环。
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
在处理update
之前,有一个优先级lane
的判断。如果当前update
对象的优先级较低,则会设置shouldSkipUpdate
变量为true
,表示应该跳过当前更新,被遗留的update
就会存储到之前所说的queue.firstBaseUpdate
属性中。
一般DOM事件触发的更新都是普通的更新【BaseUpdate
】,有足够的优先级,不会被跳过更新,我们主要掌握它的更新逻辑即可。
下面开始解析update
对象处理的具体过程:
// 1,计算state
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);
// 2,处理回调函数
const callback = update.callback;
// 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
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
对象的payload
属性计算state
。 - 处理
update
对象的callback
回调函数。
这里调用了一个getStateFromUpdate
方法来计算state
,所以我们还得查看这个方法的内容。
getStateFromUpdate
查看getStateFromUpdate
方法:
function getStateFromUpdate<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {
case ReplaceState: {
}
case CaptureUpdate: {
}
# state计算更新
case UpdateState: {
const payload = update.payload;
let partialState;
if (typeof payload === 'function') {
// Updater function
partialState = payload.call(instance, prevState, nextProps);
} else {
// Partial state object
partialState = payload;
}
if (partialState === null || partialState === undefined) {
// Null and undefined are treated as no-ops.
return prevState;
}
// Merge the partial state and the previous state.
// 返回合并生成的新对象【浅拷贝】
return assign({}, prevState, partialState);
}
// 拥有强制更新标识
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
return prevState;
}
getStateFromUpdate
方法内就是一个switch case
结构,根据update
对象的tag
值进行不同的逻辑处理。
类组件调用this.setState
创建的update
对象的属于UpdateState
场景,这里就会进入UpdateState
分支计算state
。
取出update
对象的payload
属性,前面已经讲解过payload
属性存储的就是state
数据,即this.setState
的第一个参数。
这里判断payload
是否为函数:
- 如果是函数,则直接调用
payload()
,将计算后最新的state
数据赋值给partialState
变量。
partialState = payload.call(instance, prevState, nextProps);
- 如果不是函数则为对象,直接赋值给
partialState
变量。
最后调用assign
方法,浅拷贝变化的属性,返回处理后最新的state
数据。
// 返回合并生成的新对象【浅拷贝】
return assign({}, prevState, partialState);
到此,第一点处理state
数据的逻辑就此执行完成。
下面再处理this.setState
的第二个参数:callback
回调函数。
// 2,处理回调函数
const callback = update.callback;
// 如果存在回调函数,则添加到队列的effects数组中,表示有副作用等待执行
if (callback !== null && update.lane !== NoLane) {
// 副作用标记
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else {
effects.push(update);
}
}
callback
的处理比较简单,主要就是将有callback
回调函数的update
对象存储到queue.effects
属性中,effects
属性是一个数组,专门用于存储含有副作用的update
对象,并且这里给对应的类组件节点workInProgress
打上了Callback
的flags
标记。
它的作用是在之后的commit
阶段【commitLayoutEffects
函数】中,循环执行effects
中的这些回调函数。
同时在这里我们也可以得出一个原理: 一个DOM事件中,如果调用了多次this.setState(state, cb)
,那么它cb
回调函数一定是在所有的state
计算完成之后才执行的,也就是说,在第一个cb
回调函数执行时,它使用的state
就已经是最新的state
数据了。
到这里,一个update
对象就已经处理完成了,然后从当前update
对象的next
属性中取出下一个处理的update
。
update = update.next;
只要update
有值,就会一直循环处理,直到最后一个update
对象,因为它的next
属性为null
。
if (update === null) {
break;
}
跳出do while
循环,pendingQueue
中的内容就此处理完成。
最后存储最新的newState
到queue.baseState
中作为下一次更新计算的基础state
数据。
同时更新当前类组件节点workInProgress
的memoizedState
属性,表示为当前最新的state
数据。
// 更新state
queue.baseState = newState;
workInProgress.memoizedState = newState;
到此,updateClassInstance
方法第一点逻辑,也就是最重要的计算state
执行完成。
(二)调用getDerivedStateFromProps
钩子。
...
if (typeof getDerivedStateFromProps === 'function') {
// 调用getDerivedStateFromProps钩子
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
newState = workInProgress.memoizedState;
}
...
如果类组件定义了getDerivedStateFromProps
钩子函数,则在此触发此回调。
(三)调用shouldComponentUpdate
钩子,检查组件是否应该更新。
const shouldUpdate =
// 检查是否为强制更新
checkHasForceUpdateAfterProcessing() ||
// 调用shouldComponentUpdate钩子,检查是否应该更新
checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext)
这里检查组件更新其实有两个条件:
- 检查是否为强制更新。
- 调用
shouldComponentUpdate
钩子,检查组件是否应该更新。
满足任何一个条件即返回true
,表示组件需要更新。
在强制更新场景中,shouldComponentUpdate
钩子其实是失效的,因为在||
的逻辑中,第一个状态为true
,就不会再执行后面的条件,所以shouldComponentUpdate
钩子不会被调用。
而强制更新的逻辑也很简单,checkHasForceUpdateAfterProcessing
方法仅仅是返回一个变量的状态:
export function checkHasForceUpdateAfterProcessing(): boolean {
return hasForceUpdate;
}
hasForceUpdate
是一个全局变量,它默认为false
,表示非强制更新,它的修改就在之前计算state
的逻辑中:
newState = getStateFromUpdate(...)
function getStateFromUpdate<State>() {
switch (update.tag) {
case ReplaceState: {}
case CaptureUpdate: {}
case UpdateState: {}
// 强制更新 场景
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
}
如果为this.forceUpdate
触发的场景,就会进入ForceUpdate
分支,更新变量hasForceUpdate
的值为true
。
这时在校验组件是否应该更新时,就会返回true
,代表组件需要更新。
而我们当前是通过this.setState
修改数据触发的更新,所以当前hasForceUpdate
是为false
的,这也是绝大部分类组件更新的场景。
当第一个条件为false
时,就需要执行第二个条件,调用shouldComponentUpdate
钩子来检查类组件是否需要更新。
查看checkShouldComponentUpdate
方法:
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
let shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
return shouldUpdate;
}
// 针对PureComponent纯组件的 内部校验
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
checkShouldComponentUpdate
方法也比较简单,主要就是调用一次我们定义的shouldComponentUpdate
钩子,根据调用的返回值来决定类组件是否应该更新,如果我们没有使用shouldComponentUpdate
钩子,则类组件是默认需要更新的。
同时在这里我们还可以发现有一个针对PureComponent
纯组件的更新校验,这其实就是PureComponent
和Component
唯一的区别,纯组件PureComponent
在react内部自动帮助我们对props
和state
进行了浅比较,任何一个变化则返回true
,需要更新组件。
(四)为类组件Fiber
的flags
属性设置对应生命周期钩子的副作用标记。
// 执行componentWillUpdate钩子
if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(newProps, newState, nextContext);
}
// 更新flags标记
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.flags |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
workInProgress.flags |= Snapshot;
}
如果类组件定义了componentWillUpdate
生命周期钩子函数,则会在此处调用此函数。
然后如果类组件定义了componentDidUpdate
或者getSnapshotBeforeUpdate
生命周期钩子函数,就会更新组件Fiber
的flags
属性标记。
这些生命周期钩子会在
commit
阶段,真实DOM渲染完成之后,被触发执行。
最后同步组件实例instance
的props
和state
:
instance.props = newProps;
instance.state = newState;
到此,一个类组件的更新程序基本执行完成。
commit阶段
前面全部的加载逻辑都是在Fiber Reconciler
协调流程中执行的,即类组件大部分的加载或者更新逻辑都是在reconciler
协调流程中完成的,还有剩下的一部分逻辑在commit
阶段之中处理,这里我们继续讲解。
这里简单介绍一下
commit
阶段的内容,更多处理逻辑可以查看《React18.2x源码解析(四)commit阶段》。
commit阶段的逻辑主要分为三个子阶段内容:
- BeforeMutation
- Mutation
- Layout
function commitRootImpl() {
// 1,BeforeMutation阶段
commitBeforeMutationEffects()
// 2,Mutation阶段,渲染真实DOM加载到页面
commitMutationEffects()
// 3,Layout阶段
commitLayoutEffects()
}
对于类组件的更新来说,在commit
阶段主要还有以下两部分逻辑需要处理:
- 执行类组件的
componentDidUpdate
生命周期钩子函数。 - 执行
this.setState
方法传入的回调函数。
这两部分逻辑都是在Layout
阶段中处理的,下面我们查看具体的处理逻辑:
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型:进行不同的处理
switch (finishedWork.tag) {
// 类组件的处理
case ClassComponent: {
// 组件实例
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// mount加载阶段
# 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
instance.componentDidMount();
} else {
// update更新阶段
const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
: resolveDefaultProps( finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
# 触发componentDidUpdate 生命周期钩子函数
instance.componentDidUpdate( prevProps,prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
# 取出当前组件节点的updateQueue更新对象
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
...
}
}
}
finishedWork
代表当前处理的类组件Fiber
节点,首先从fiber.stateNode
属性中取出组件实例instance
。然后根据fiber.flags
进行判断,只有存在相关的副作用标记才会继续内部的逻辑:
if (finishedWork.flags & Update) {
...
}
当前条件是满足的,只要定义了相关的钩子函数,就会在之前的reconciler
协调流程中被标记,它的作用就是在此刻进行判断,然后执行相关的副作用回调。
然后判断current
是否为null
,current
表示旧的虚拟DOM节点,在组件的更新阶段,它肯定是存在的。
if (current === null) {
// 加载
} else {
// 更新
instance.componentDidUpdate(prevProps, prevState);
}
然后直接调用componentDidUpdate
生命周期钩子函数即可。
下面我们再继续查看对this.setState
回调的处理。
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
类组件更新阶段updateQueue
属性都是有值的,直接查看commitUpdateQueue
方法。
commitUpdateQueue
// packages\react-reconciler\src\ReactFiberClassUpdateQueue.new.js
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
): void {
// Commit the effects
const effects = finishedQueue.effects;
// 重置
finishedQueue.effects = null;
if (effects !== null) {
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
}
}
}
这里从updateQueue
属性中取出effects
副作用回调数组,然后立即重置为null
【已经拿到内容,不再需要保留,重置后方便下次更新重新添加】。
接着判断effects
是否为null
,很明显当前是有值的,因为我们之前调用了两次this.setState
都传递了回调函数,所以当前的effects
数组应该会有两个元素内容,以调试截图印证:
effects
数组有值,则循环effects
数组:
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
// 执行回调
callCallback(callback, instance);
}
}
从中取出effect
对象,其实就是之前的update
对象,然后取出callback
回调函数,最后触发回调函数。
callback()
循环结束,控制台依次打印出我们设置的日志:
到此类组件的更新内容全部执行完成。
总结
类组件的更新逻辑重点内容是以下两点:
reconciler
协调流程中循环update
链表计算出最新的state
。commit
阶段中触发componentDidUpdate
生命周期钩子函数以及循环执行this.setState
的回调。
转载自:https://juejin.cn/post/7280435532987842615