一、前言
react runtime分为三个阶段
- scheduler调度,调度首次渲染、更新等任务
- reconcile协调,创建fiber(fiber是什么, fiber如何生成),进行diff算法等
- render渲染,最终渲染到页面 此次文章主要讲的是react如何将fiber渲染到页面,以及渲染中都发生了什么。
二、effectList
1. effectList是什么
- effectList正如这个单词表达的意思一样,它是记录此次更新中的副作用的,这里的副作用指的是在render阶段需要操作的dom,包含新增、删除、更新等。
- effectList是一个环状链表结构的数据,其中每个effect的nextEffect都指向下一个effect,而这里的effect其实就是存在flags(flags是以32位二进制存储的,采用的是位运算)不等于NoFlags的fiber,就如我们在diff算法中所说的一致
- 在reconcile结束后,workInProgress rootFiber(即rootFiber.alternate)的firstEffect指针指向effectList的第一个effect
2. effectList解决什么问题
我们知道,reactDom在render阶段是处理存在effect的fiber,如果是从workInProgress rootFiber开始遍历寻找存在effect的fiber势必会造成性能的损耗,所以在reconcile阶段会生成一个effectList,在render阶段只要遍历这个effectList,提升性能
3. effectList何时生成
在diff算法中,有提到effectList是在completeWork时生成的,每当completeWork处理完一个fiber时,如果这个fiber存在effect,就会将effect加入effectList这个链表。
// do while循环处理effect
// 使每个returnFiber上的firstEffect都指向effectList的第一个effect
// lastEffect指针指向effectList的最后一个effect
// 每个effect的nextEffect都指向下一个effect
// lastEffect的nextEffect指向firstEffect
do {
var current = completedWork.alternate;
var returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentFiber(completedWork);
var next = void 0;
// completeWork,返回next
if ( (completedWork.mode & ProfileMode) === NoMode) {
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentFiber();
// 如果存在next fiber,退出调用栈,进行beginWork
if (next !== null) {
workInProgress = next;
return;
}
resetChildLanes(completedWork);
// 处理effect
if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
// 如果已存在的effect
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
var flags = completedWork.flags;
// 处理此次completedWork的effect,如果有的话
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// 此次completeWork存在异常
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
如上代码所示,最终我们在render阶段就可以通过workInProgress rootFiber的firstEffect拿到effectList,然后开始渲染
三、commit三次循环
render开始于commitRootImpl函数
function commitRootImpl(root, renderPriorityLevel) {
if (firstEffect !== null) {
// ...
// 第一次循环,操作dom之前
do {
{
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
// ...
}
} while (nextEffect !== null);
// ...
nextEffect = firstEffect;
// 第二次循环,操作dom
do {
{
invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
// ...
}
} while (nextEffect !== null);
// ...
root.current = finishedWork;
nextEffect = firstEffect;
// 第三次循环,操作dom之后
do {
{
invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
// ...
}
} while (nextEffect !== null);
// ...
} else {
// do something
}
}
从上面代码可知,commitRootImpl经历了三次对effectList的do while循环,分别处理不同的逻辑,reactDom的渲染阶段也是在这三个循环中完成,三个循环分别对应的处理函数是commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects,接下来看看这三个处理函数都干了什么
1.commitBeforeMutationEffects
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
var current = nextEffect.alternate;
// 处理input focus blur相关
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
}
var flags = nextEffect.flags;
// 主要调用classComponent类型组件的getSnapshotBeforeUpdate
if ((flags & Snapshot) !== NoFlags) {
setCurrentFiber(nextEffect);
commitBeforeMutationLifeCycles(current, nextEffect);
resetCurrentFiber();
}
// 异步调度useEffect产生的effect
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// scheduleCallBack是调度器schedule相关,可以异步调度一个函数
scheduleCallback(NormalPriority$1, function () {
// 处理useEffect产生的effect
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
从代码可知,在render的第一次循环中,主要做了三件事
- 如果是input,处理input相关的focus,blur相关事件
- 如果是classComponent类型组件,也就是通过class方式创建的组件,会调用getSnapshotBeforeUpdate这个生命周期函数,也就是在dom未发生改变之前执行
- 如果存在useEffect,异步调度effect的处理函数flushPassiveEffects,reactDom render阶段是同步执行的,所以从而得知useEffect的回调函数是在组件渲染完成后执行的
2.commitMutationEffects
function commitMutationEffects(root, renderPriorityLevel) {
while (nextEffect !== null) {
setCurrentFiber(nextEffect);
var flags = nextEffect.flags;
// 清空子节点是文本节点内容,如textarea、option、noscript、dangerouslySetInnerHtml等
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
// 如果存在ref,清空原来的ref
if (flags & Ref) {
var current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
// 根据effect flags,调用不同的处理逻辑
switch (primaryFlags) {
// 插入
case Placement:
{
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
}
// 插入更新
case PlacementAndUpdate:
{
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
break;
}
// 服务端渲染相关
case Hydrating:
{
nextEffect.flags &= ~Hydrating;
break;
}
// 服务端渲染相关
case HydratingAndUpdate:
{
nextEffect.flags &= ~Hydrating;
var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
break;
}
// 更新
case Update:
{
var _current3 = nextEffect.alternate;
commitWork(_current3, nextEffect);
break;
}
// 删除
case Deletion:
{
commitDeletion(root, nextEffect);
break;
}
}
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
}
第二次循环,commitBeforeMutationEffects也做了四件事
- 文本类型节点的清空操作
- ref的清除
- 函数组件执行useLayoutEffect销毁函数,如果有的话
- 根据effect的flags,进行相应的dom操作,这里的相应的处理函数不做详细介绍,逻辑也比较简单,大家可以自行去看一下,commitPlacement最终调用的是appendChild或者insertBefore,commitWork最终根据effect的updateQueue更新dom属性,commitDeletion最终调用removeChild 至此,render阶段已经发生了dom的变化,包含更新、新增、删除
3.commitLayoutEffects
在第三次循环之前,有一行代码
root.current = finishedWork;
这也就验证了在react fiber概念及原理所说的,reactDom存在两颗fiber树,在渲染结束后,会将rootFiberNode的current指针指向workInProgress fiber,而current fiber将作为下一次的更新所用
function commitLayoutEffects(root, committedLanes) {
{
markLayoutEffectsStarted(committedLanes);
}
while (nextEffect !== null) {
setCurrentFiber(nextEffect);
var flags = nextEffect.flags;
if (flags & (Update | Callback)) {
var current = nextEffect.alternate;
// 处理useLayoutEffect
// 将useEffect回调函数及销毁函数推入队列,等待异步调用
commitLifeCycles(root, current, nextEffect);
}
// ref赋值的相关操作
{
if (flags & Ref) {
commitAttachRef(nextEffect);
}
}
resetCurrentFiber();
nextEffect = nextEffect.nextEffect;
}
{
markLayoutEffectsStopped();
}
}
进入最后一次循环,主要是做了两件事
- 调用commitLifeCycles
- ref赋值的相关操作 这里我们重点看一下commitLifeCycles这个函数
function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
{
{
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent:
{
var instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
{
// ...
{
instance.componentDidMount();
}
} else {
// ...
{
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
}
}
}
// ...
return;
}
case HostRoot:
{
// ...
commitUpdateQueue(finishedWork, _updateQueue, _instance);
}
return;
}
case HostComponent:
{
// ...
}
case HostText:
{
// ...
}
case HostPortal:
{
// ...
}
case Profiler:
{
// ...
}
case SuspenseComponent:
{
// ...
}
case SuspenseListComponent:
case IncompleteClassComponent:
case FundamentalComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent:
return;
}
// ...
}
commitLifeCycles根据不同类型的组件调用相应的处理逻辑,这里主要分析一下FunctionComponent,ClassComponent以及HostComponent
- FunctionComponent
主要是做了两件事
第一件事是调用commitHookEffectListMount,处理useLayoutEffect,可以发现,useLayoutEffect是同步调用的,也就如官方文档所描述的一样,具体逻辑大家可以详细看一下。
第二件事是将所有的useEffect和上次更新的useEffect销毁函数(也就是useEffect的两个参数)分别推入队列pendingPassiveHookEffectsMount和pendingPassiveHookEffectsUnmount,这两个队列的第i项是对应的effect,第i+1项是对应的fiber。而这两个队列的作用就是提供给第一次循环中的第三件事使用,即flushPassiveEffects,它会循环遍历这两个队列,执行其中的副作用。 - ClassComponent 如果是ClassComponent类型组件,如果是第一次渲染,会调用componentDidMount生命周期,如果是更新,则调用componentDidUpdate生命周期,最后如果存在setState第二个参数,则执行它
- HostComponent HostComponent即ReactDom.render(或其他模式模式下render,如concurrent模式下createRoot模式创建的应用)组件,在此次循环会执行该组件的回调函数,对于ReactDom.render来说,就是对应的第三个参数
四、render最后
在render最后,reactDom会再次调度一次更新
ensureRootIsScheduled(root, now());
至此,react reactDom的reconcile、render两个阶段有了一个完整解读。当然,几篇文章不足以描述react源码,其中还有许多细节需要去多多debugger。