最近在学习react源码。经过接近两周的时间,目前梳理出来了现阶段fiber的工作过程以及Function和ClassComponent的工作细节。其他方面暂未深入。写此篇文章总结一下学习的所得。
后面的内容中会根据需要附上部分源码,为了便于大家对比学习,先说明一下源码版本:
"react": "16.13.1",
"react-dom": "16.13.1",
先上结论:react的本质是一套复杂的任务管理系统。fiber是一套复杂的任务调度方案
为什么会这么说?在进入本文正文之前,我想先举个例子来做一个铺垫。
引例
在实际的开发过程中,大家可能会面临处理任务队列的场景。这个时候,可能会这么实现。
- 任务队列(TaskQueue):可能是个数组或者链表
- 创建新任务的方法(createTask):创建一个任务(Task),一般会是个对象,记录本次任务相关的数据
- 创建新任务元素的方法(createTaskElement):创建一个任务元素(TaskElement),一般会是个对象,记录本次任务相关的数据,以及任务队列需要使用的相关数据
- 任务入队的方法(inQueue): 往任务池内添加一个任务元素
- 获取队列中第一个元素的方法(seek):复制任务队列中第一个元素
- 处理任务的方法(handleTask):处理任务
- 任务处理成功的回调(onTaskSuccess):顺利处理完任务,消耗掉一个任务
- 任务处理失败的回调(onTaskFailed): 处理过程里出现异常,该任务没消耗掉。
- 任务出队的方法(outQueue):从任务池里取出一个元素
- 事件循环(loop): 通过循环不断地执行整个过程。
现在我们已经通过队列实现了一个最简单的任务管理了。
这个时候产品经理过来,新增一个需求:现在体验不太好,有的任务执行完以后,需要做点别的处理,现在想做点事情做不了。给每个任务添加执行成功后的回调,执行成功后,都需要执行一下对应的回调。
现在实现了一个支持任务执行成功执行回调的需求了
产品再次新增需求:需要某些任务有更高的优先级。高优先级的任务优先执行。
这个时候我们再次调整一下整个流程
现在每次取出来任务执行的时候,都会先按照优先级的关系,如果高优先级的队列里有任务,先处理高优先级的,没有高优先级的,才处理低优先级的任务。
整个任务管理方式已经不像一开始那么简单了。
产品经理又过来了,说现在体验还是不够好,现在如果正在执行低优先级任务,这个时候有高优先级任务进来,需要等待上一个任务执行完,下一个任务才是高优先级任务,能不能支持一旦有高优先级的任务进来,如果正在执行的任务优先级低于新产生的任务,直接打断当前执行,同时撤销对当前任务的改动,转而去处理新产生的任务。等没有高优先级的任务后,再回过头来执行低优先级任务。
我们再次调整整个流程
现在整个流程已经有点复杂,但是整体还能接受。
这个时候产品经理,又过来了,再加一个需求: 我觉得现在的这套任务机制挺好用的,现在有其他几个模块,能不能也采用这套任务管理方案,但是需要所有模块产生的任务,统一调度执行。就是在同一个事件循环里处理不同模块产生的不同优先级的任务。
我们继续尝试着拓展我们的任务管理流程
现在已经支持多模块了。
然而这个时候产品经理,又过来了,再加一个需求: 目前多模块使用过程中发现一些问题。各个模块之间的任务,有时候执行顺序有问题。能不能也给不同的模块给个优先级呢?然后列出来各个模块的优先级关系。
此次调整比较简单,按照预定的模块优先级关系,改变事件循环中模块的遍历顺序即可。流程图偷个懒,不画了 。基本和上图一样,就是遍历模块的时候,顺序规定一下。
目前整个任务管理已经到了多模块,多优先级,可打断,可插队的阶段。请再次回忆一下整个过程,是怎么从一个简单的任务队列演变到现在的。
我们总结一下整个例子:
我们可以把整个工作过程划分为三个阶段:
- 阶段一:创建并添加任务到任务池
- 阶段二: 调度任务池中的所有任务,分为任务入队的调度和出队的调度。
- 阶段三:(文中的流程图中没有体现该阶段)在整个事件循环过程中,不断地有任务添加进来,也不断的有任务被执行完毕,执行完毕的任务,在某些时间点会将任务结果阶段性的提交给业务方处理。
请牢记这几个阶段,后面会用到。
为什么要花费这么多的篇幅来举这么个例子。因为这个例子继续拓展下去,就会逐渐的演变成react的工作流程,整个react就是这么一套创建任务,执行任务的逻辑。react16的fiber是阶段二使用的所有任务的调度算法。
这个例子继续演变下去:
- 模块分层:所有的模块划分层级关系,形成一个树结构。每个模块都会对应一个组件。
- 任务分类:整体先分为同步任务、异步任务等,每一类任务的优先级再次细分,分为空闲时段执行的任务,用户交互产生的任务等。同时所有模块的所有任务,统一使用过期时间来区分执行的先后顺序。每次产生新任务的时候,都根据任务的类型,匹配不同的计算方法,计算出来任务的过期时间。然后在事件循环中,每次找出来所有模块所有任务中在当前时间点所有的过期任务,然后全部执行完毕。
- 添加更多的回调:除了每个任务支持成功的回调以外,每个队列执行成功后和每个模块的任务在执行的不同阶段,也支持回调。这些回调最终会对应到setState的回调,以及组件的声明周期方法。
- 任务处理成功的后续:单一模块上的任务执行完毕后,需要做一些后续的处理逻辑。react则最终会刷新页面。
- 关于异常的处理: 在整个流程中,任何一个环节出错,应该怎么处理。对应react项目内的警告,错误提示灯
等等等等。
例子介绍到这里,这一阶段算是到了尾声,这里要结合例子中的概念引出来几个react及fiber中的几个重要概念:
- workLoop: 直译,事件循环。对应我们的例子中的事件循环
- unitOfWork(Fiber):直译,工作单元。是一个Fiber对象。对应我们的例子中的一个模块。该对象内有一个updateQueue字段,通过链表记录所有的任务。react中事件循环是以fiber作为任务的工作单元的。
- update: 直译,更新。从词性上讲是一个名词。可以理解为是一个个的任务。对应案例里面的各个模块中队列里的任务元素。在Fiber中存放在updateQueue中。
- ReactElement:虚拟Dom(vDom)。是一个不同于Fiber的对象。每一个vDom最终都会和一个Fiber一一对应。
- expirationTime:直译,过期时间。用来区分每个任务的优先顺序的,哪个任务更早过期,哪个任务需要更早执行。
结合源码理解react和fiber
还记得一开始我抛出来的结论吧:
react的本质是一套复杂的任务管理系统。fiber是一套复杂的任务调度方案
为什么这么说?下面我们开始进入react的源码部分,我们结合源码去理解这个事情。
推荐大家先下载一份v16.13.1源码,便于根据文内的代码位置,查看相关的源码。
为了便于快速理解,代码内必要的地方我会逐行注释
我们先看一短最简单的代码
import React, { Component } from 'react'
export default class StateComponent extends Component {
state = {
num: 1,
}
onClick = () => {
const { num } = this.state;
this.setState({num: num + 1});
}
render() {
const { num } = this.state;
return (
<div>
<div>{num}</div>
<button onClick={this.onClick}>按钮</button>
</div>
)
}
}
一个很简单的状态组件
作为一个react的使用者,肯定都知道setState以后,会触发页面刷新,渲染出最新的state值。不知道你有没有好奇过当你调用setState后,发生了什么?
在往下阅读前,建议停下来几秒钟,自己先设想一下这个问题,如果是你要实现上述效果,这个方法内你会做什么。然后结合自己思考的结果,继续往下看,效果会更好。
react中调用setState发生了什么?
不废话,上源码
这里需要多个部分的源码:Component,setState
Component
packages/react/src/ReactBaseClasses.js line20
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
setState
packages/react/src/ReactBaseClasses.js line57
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
看到了吗?component就是一个对象,对象原型上有setState这个方法。
我们调用的setState,实际上就是执行了
this.updater.enqueueSetState(this, partialState, callback, 'setState');
我当时看到这里的时候,心里一万个卧槽和一万零一个疑问,这就完了?这就是Component?生命周期方法呢?这就是setState?这个updater是啥?enqueueSetState又是啥?不知道此时的你,有没有和我一样的想法。
带着疑问,我们继续
updater
packages/react-reconciler/src/ReactFiberClassComponent.js line181
const classComponentUpdater = {
// inst 是调用setState方法的那个组件对应的对象,就是虚拟Dom对象
// payload 是调用setState方法的传入的第一个参数,一般是新的state对象或者是一个函数
// setState执行成功后对应的回调函数
enqueueSetState(inst, payload, callback) {
// 根据vDom获取其对应的fiber对象,vDom和fiber是一一对应的
const fiber = getInstance(inst);
// 计算当前的时间
const currentTime = requestCurrentTimeForUpdate();
// 获取suspense对应的配置信息,暂时可忽略
const suspenseConfig = requestCurrentSuspenseConfig();
// 计算过期时间
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 创建update任务
const update = createUpdate(expirationTime, suspenseConfig);
// 设置任务的实际工作内容
update.payload = payload;
// 判断是否有回调函数
if (callback !== undefined && callback !== null) {
// 开发模式下,一些警告的提醒内容,可以略过
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
// 将回调也赋值给任务
update.callback = callback;
}
// 任务入队
enqueueUpdate(fiber, update);
// 开始调度任务
scheduleWork(fiber, expirationTime);
},
}
看到这里,是不是觉得有那么点熟悉,和我刚刚讲的例子是不是有那么点的相似??
这里结合开头的例子来解释一下。setState的本质,就是创建了一个更新任务,并将其添加到任务队列中。对应案例的阶段一。
我们继续往下看,看看enqueueUpdate(fiber, update);
做了什么,看名字就知道了,入队。我们看看细节
packages/react-reconsiler/src/ReactUpdateQueue.js line205
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// 取出来调用setState方法的当前组件上对应的fiber对象中的更新队列的值
const updateQueue = fiber.updateQueue;
// 如果队列为空,直接返回,只发生在整个项目初始化的阶段
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
// 目前我还没找到这里为什么要添加shared这层,有清楚的大佬,留言指点一下。thanks~~
const sharedQueue = updateQueue.shared;
// pending是指本次事件循环内,新增的任务,是一个环形链表。
// pending指向当前链表的最后一个元素
const pending = sharedQueue.pending;
// 如果链表最后一个元素为空
if (pending === null) {
// 先让当前的update形成一个环,自己的next指向自己
// This is the first update. Create a circular list.
update.next = update;
} else {
// 如果最后一个元素不为空,就将新添加的update放在环形链表的最后一个位置。
update.next = pending.next;
pending.next = update;
}
// 重新让pending指针指向环形链表的最后一个元素
sharedQueue.pending = update;
if (__DEV__) {
if (
currentlyProcessingQueue === sharedQueue &&
!didWarnUpdateInsideUpdate
) {
console.error(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
didWarnUpdateInsideUpdate = true;
}
}
}
然后再看一下scheduleWork(fiber, expirationTime);
做了什么,
packages/react-reconciler/src/ReactFiberWorkLoop.js line449
scheduleWork
是scheduleUpdateOnFiber
重命名。直译一下就是:安排工作 = 安排Fiber上的update。
export const scheduleWork = scheduleUpdateOnFiber;
packages/react-reconciler/src/ReactFiberWorkLoop.js line379
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
) {
// 检查嵌套层级,可以理解为做一下相关的校验好检查工作
checkForNestedUpdates();
// DEV模式下使用的用来抛出一些警告信息,可以忽略
warnAboutRenderPhaseUpdatesInDEV(fiber);
// 这个方法内通过递归的方法,从当前节点开始,逐步对比并修改父Fiber的过期时间数据的。
// 大家可以自行查看。
// 基本逻辑是:如果当前任务的过期时间早于父节点记录的childExpirationTime,即所有子节点的已知任务的最早过期时间,就修改为当前的。
// 意味着每个父节点都会记录自己所有子节点所有任务中,最早过期的那个任务对应的过期时间。
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
// 也是抛警告的代码
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
// 检查任务是不是被打断,里面记录被哪个任务打断的
checkForInterruption(fiber, expirationTime);
// 会标记几个值,还没找到啥作用,不影响对本文的理解。有大佬请指点一下。
recordScheduleUpdate();
// TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
// 获取当前工作的优先级
const priorityLevel = getCurrentPriorityLevel();
// 如果是同步任务,就是需要立即执行的任务,坦白讲我也只梳理出来同步任务相关的内容。异步的还在啃
if (expirationTime === Sync) {
if (
// Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
// 这里进入任务执行阶段。
// 每次都是从树的根节点作为入口,然后挨个往下找,找到要执行的任务,然后开始执行。
// 然后继续找下一个任务,继续执行。
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority ||
priorityLevel === ImmediatePriority)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
这里做的事情就是对应阶段二里面的开始执行任务。是不是和前面的例子更像了?
我们再去找一找,例子里面,执行完一个模块,再去执行下一个模块的逻辑:
(省略一些非关键代码)
packages/react-reconciler/src/ReactFiberWorkLoop.js line990
function performSyncWorkOnRoot(root) {
...
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
...
}
packages/react-reconciler/src/ReactFiberWorkLoop.js line1459
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
packages/react-reconciler/src/ReactFiberWorkLoop.js line1459
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
startWorkTimer(unitOfWork);
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderExpirationTime);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
进入了一个循环,每次都把当前fiber的子节点返回作为下一个unitOfWork。
介绍到这里,例子中的各个部分基本上都能对照源码解释一下了。可见整个react就是一个复杂的任务管理方案。不断地创建任务,调度任务,然后执行任务。
本文重点是在于介绍react的理论模型。源码部分仅作为补充对照的说明。暂时不对源码做深入分析。(因为我也没吃透。。。)
希望本文能对学习react源码提供一定的帮助。如果感受到了有用,欢迎点赞转发分享。鉴于水平有限,文中如果有错误的地方,也请大佬留言指正。共同进步。