react渲染分为render阶段 和commit阶段
本篇我们主要了解下render阶段,react主要做了什么,并发和同步模式下虽然函数调用链不同,但核心的还是那几个函数,我们以核心函数做讲解。
什么是render阶段
render阶段主要是深度优先遍历我们的dom结构,生成一颗fiber树,是采用递归的形式进行的。从页面的根节点(一个页面只有一个根节点),rootFiber开始,向下查找它的第一个子节点,生成对应的fiber节点,并挂载到这颗rootFiber的child属性上,并在这个阶段进行diff比较,为fiber节点打上标记flag(更新、新增、删除等),以此类推,直到末级节点,再向上归溯,这个过程会生成节点对应的dom实例,如果是新增直接生成dom节点到fiber节点的stateNode上,节点发生更新则会生成updateQueue,结构如下:updateQueue: (2) ['children', '7']; 并将dom实例,挂载到虚拟dom树上,直到向上归溯到根节点 rootFiber。这就生成了一棵完整的fiber树
对于fiber结构不太了解的可以查看另一篇文章 juejin.cn/editor/draf…
render阶段,执行了很多函数,其中workLoopSync就是执行递归的入口函数
render阶段“递归”的对象是什么? 是带状态的fiber节点
递的主要入口函数为beginWork,归的主要入口函数completeWork,这两个函数具体做了什么?形成了什么产物?
render阶段的函数调用情况
我们创建一个调试项目,npm start后打开控制台
我们的页面结构如下:
这里点击p标签更新num,将会改变code中的数字,以此观察react在页面初始和state更新时,源码的执行情况。
启动项目,进入浏览器,打开控制台 录制performance,可以看到整体函数的调用情况
我们将函数调用分为两种情况,一个是初始化,一个是更新 红色部分就是render阶段执行的函数,绿色部分为commit阶段执行的函数,打开call tree我们可以看到更直观的函数调用栈
初始化
更新
看到这么多函数调用,我脑瓜子嗡嗡的,这还折叠了一些,看来我离资深又进了一步(头发越来越稀少了)
我们可以看到更新和初始化,两者的函数调用栈有所不同,这是因为react18引入了并发 concurrent并发的概念,这里初始化时,默认为并发模式,render的主要入口为performConcurrentWorkOnRoot,下面的执行栈也与更新有所不同,而更新时为同步模式,render的主要入口为performSyncWorkOnRoot 虽然主要入口有所不同,但执行render递归的核心函数还是一样的(renderRootSync、workLoopSync、performUnitWork、beginWork、completeWork、commitRoot等)
为啥会有这个区别呢,待研究。。。
并发模式和同步模式有什么区别呢?
接下来我们结合performanc看看完成render流程的主要函数
render阶段的主要函数讲解
workInProgress
这是react的一个全局变量,用于存储当前处理的fiber节点
调度入口ensureRootIsScheduled
这个函数是render的入口函数,内部会判断当前render任务的优先级,如果有更高优先级的任务,当前任务会被打断,其次会判断当前更新是同步还是并发,同步通过syncQueue中插入performSyncWorkOnRoot,然后再通过##### flushSyncCallbacks将syncQueue中的任务依次执行,并发下则通过Scheduler(Scheduler模块)来调度performConcurrentWorkOnRoot任务
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackNode = root.callbackNode; // 当前render阶段的任务
markStarvedLanesAsExpired(root, currentTime); // 获取root节点上的未执行的任务,并遍历出过期任务,并标记它们
// 获取需要执行下一个赛道
var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
// 赛道为空,代表不需要执行当前阶段的任务,通过schduler删除当前任务
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback$1(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 比较当前赛道与其他赛道,得到最高优先级的
var newCallbackPriority = getHighestPriorityLane(nextLanes);
// 判断是同步任务还是并发任务
if (includesSyncLane(newCallbackPriority)) {
// 这里同步任务放在单独的队列中执行,不通过schduler直接调度
if (root.tag === LegacyRoot) {
if ( ReactCurrentActQueue$2.isBatchingLegacy !== null) {
ReactCurrentActQueue$2.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
{
// 将队列放入微任务中执行
if ( ReactCurrentActQueue$2.current !== null) {
ReactCurrentActQueue$2.current.push(flushSyncCallbacks);
} else {
scheduleMicrotask(function () {
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbacks();
}
});
}
}
newCallbackNode = null;
} else { // 并发任务
// 会比较当前赛道的优先级
var schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdlePriority;
break;
default:
schedulerPriorityLevel = NormalPriority;
break;
}
// 通过Scheduler调度任务
newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
scheduleSyncCallback
我们可以看到scheduleSyncCallback只是将任务推入栈中,并没有其他操作
function scheduleSyncCallback(callback) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
if (syncQueue === null) {
syncQueue = [callback];
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
syncQueue.push(callback);
}
}
function scheduleLegacySyncCallback(callback) {
includesLegacySyncCallbacks = true; // 这是标志的什么?
scheduleSyncCallback(callback);
}
接下来我们讲下scheduleMicrotask和flushSyncCallbacks这两个函数的作用
scheduleMicrotask
// 判断浏览器promise是否支持
var localPromise = typeof Promise === 'function' ? Promise : undefined;
// 判断queueMicrotask是否支持,不支持则通过promise创建微任务
var scheduleMicrotask = typeof queueMicrotask === 'function' ?
queueMicrotask : typeof localPromise !== 'undefined' ? function (callback) {
return localPromise.resolve(null).then(callback).catch(handleErrorInNextTick);
} : scheduleTimeout; // TODO: Determine the best fallback here.
function handleErrorInNextTick(error) {
setTimeout(function () {
throw error;
});
}
queueMicroTask是window对象上的一个方法,developer.mozilla.org/zh-CN/docs/… 它通过使用立即 resolve 的 promise 创建一个微任务(microtask),如果无法创建 promise,则回落(fallback)到使用
setTimeout()
这里scheduleMicrotask的目的是通过queueMicroTask或promise创建一个微任务,并将flushSyncCallbacks做为微任务的第一个回调,其目的是让浏览器优先执行flushSyncCallbacks
flushSyncCallbacks
我们看到flushSyncCallbacks实际上是将ensureRootIsScheduled推入syncQueue队列中的函数一一执行,这里指performSyncWorkOnRoot,
performSyncWorkOnRoot
主要目的是执行renderRootSync
// 同步渲染
function performSyncWorkOnRoot(root) {
// 记录更新行为
{
syncNestedUpdateFlag();
}
// 判断render环境
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// 执行所有effect tag任务
flushPassiveEffects();
// 获取下一个赛道
var lanes = getNextLanes(root, NoLanes);
// 不存在赛道,则重新执行ensureRootIsScheduled,确保所有更新被执行
if (!includesSyncLane(lanes)) {
// There's no remaining sync work left.
ensureRootIsScheduled(root, now());
return null;
}
// 重点,开始执行render,并保存渲染状态
var exitStatus = renderRootSync(root, lanes);
// 渲染出错的措施
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
var originallyAttemptedLanes = lanes;
var errorRetryLanes = getLanesToRetrySynchronouslyOnError(root, originallyAttemptedLanes);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
// 在这里会再次调用renderRootSync
exitStatus = recoverFromConcurrentError(root, originallyAttemptedLanes, errorRetryLanes);
}
}
// 再次出错,重新调用入口函数,并抛出错误跳出当前执行
if (exitStatus === RootFatalErrored) {
var fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended$1(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
if (exitStatus === RootDidNotComplete) {
// 渲染未完成,需要退出当前渲染,
markRootSuspended$1(root, lanes);
ensureRootIsScheduled(root, now());
return null;
}
// render结束,将render阶段生成的最新fiber树赋值到进入commitRoot
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
// 在退出前需要确保所有未执行的调度任务被执行,
ensureRootIsScheduled(root, now());
return null;
}
performConcurrentWorkOnRoot
大部分处理同performSyncWorkOnRoot,这里会判断shouldTimeSlice 是否需要切片来决定执行renderRootConcurrent还renderRootSync
renderRootSync
主要目的是执行workLoopSync
renderRootConcurrent
主要目的是执行workLoopConCurrent
workLoopSync
同步执行performUnitOfWork 生成fiber节点
workLoopConCurrent
可以看到与workLoopSync有一点不同,当判断shouldYield为false,将暂停当前处理,并发处理
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
结合workLoopSync或workLoopConCurrent,执行beginWork,beginWork返回当前fiber节点的子节点(child) 当返回了子节点,则改变外部变量workInProgress的值为child workLoopSync继续循环 当子节点为null,也就是当前节点不存在子节点时,执行completeUnitOfWork
function performUnitOfWork(unitOfWork) {
// 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.
var current = unitOfWork.alternate;
setCurrentFiber(unitOfWork);
var next; // 用于储存当前节点的子节点
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
// 执行beginwork,并返回当前节点的子节点
next = beginWork$1(current, unitOfWork, renderLanes$1);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork$1(current, unitOfWork, renderLanes$1);
}
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 当节点不存在子节点(末级节点),则从当前节点开始执行completeWork
completeUnitOfWork(unitOfWork);
} else {
// 存在子节点,则将workInProgress赋值为子节点,使得workLoopSync/workLoopConCurrent继续循环
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}
beginwork
目的是创建当前节点的第一个属性值为child的fiber节点,首先会判断当前fiber节点类型做不同操作,进入不同更新逻辑。
这里在执行到函数方法时,如果页面上已经存在fiber节点,会进入updateFunctionComponent函数
updateFunctionComponent
这里会判断页面上是否已经存在fiber树(current)并且当前节点没有被更新,决定新建还是复制对应的fiber节点,存在时复制页面中的fiber到正在执行的fiber,否则就执行reconcileChildren
bailoutOnAlreadyFinishedWork
cloneChildFibers
其中cloneChildFibers就是克隆对应的fiber节点,其中执行了createWorkInProgress
我们可以看到这里clone的时候,是将当前节点下的子节点都进行clone,子节点存在兄弟节点时,也会循环将其clone,是通过creatWorkInProgress创建
createWorkInProgress
在这个方法其目的是创建fiber节点,当当前节点存在alternate属性,则将alternate对应的fiber节点属性复制,如果不存在则新建fiber节点
var createFiber = function (tag, pendingProps, key, mode) {
return new FiberNode(tag, pendingProps, key, mode);
};
function FiberNode(tag, pendingProps, key, mode) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode; // Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
{
// 这段是为了解决V8下的性能下降的问题
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
{
// This isn't directly used but is handy for debugging internals:
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
reconcileChildren
其余节点类型,主要是通过执行reconcileChildren方法,在这个方法中判断current属性是否为null,为null时创建,并且不标记child节点的effectTag属性,。不为null则更新当前节点的child 值,并标记effectTag属性,
创建调用mountChildFibers方法。更新调用reconcileChildFibers,注mountChildFibers和reconcileChildFibers其实都是调用同一个方法 createChildReconciler方法,只是参数一个为true,一个为false,
createChildReconciler中也存在一个函数名为reconcileChildFibers的函数,此函数中 会判断要处理的子节点类型。执行不同的创建fiber节点操作。并为节点打上effectTag标签,方便commit阶段做相应的增删改操作
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));
case REACT_LAZY_TYPE:
var payload = newChild._payload;
var init = newChild._init; // TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes);
}
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
if (typeof newChild === 'string' && newChild !== '' || typeof newChild === 'number') {
return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
}
{
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
}
这里判断了子节点类型做不同操作 当子节点是单一的react元素时,最终也是执行了上面提到的createWorkInProgress创建fiber节点
shouldTrackSideEffects为mountChildFibers或reconcileChildFibers传入的布尔值,这里为true时,并且找不到alternate属性,也就是只有元素新增时,才会打上新增的标记
当子节点是数组时
可以看到在createChildReconciler函数中,当子节点为数组时,执行了reconcileChildrenArray方法,处理的对象为子节点数组 pendingProps 这里以处理的节点为header对应时,数据结构如下
深度优先遍历:需要注意的是beginWork操作的节点下存在多个平行的子节点,也会只生成的第一个子节点的fiber。子节点的兄弟元素,则存放在子fiber节点的sibling属性中,这是一个链表结构 以App为例。当workInProgress为header时,header存在img,p,a三个子元素数组 这里当reconcileChildrenArray处理这个子元素数组时,只会生成第一个子元素,也就是img的fiber,此时img对应的fiber节点为
其中sibling则存储img的下一个兄弟元素 p的fiber节点,p中的sibling则对应 p的下一个兄弟元素 a的fiber节点,以此类推。这个sibling会在completeUnitOfWork中发挥作用
completeUnitOfWork
入参为当前操作的fiber节点,也就是workInProgress
目的是为了改变workInProgress,将workLoopSync继续或者停止
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork;
do {
var current = completedWork.alternate;
var returnFiber = completedWork.return; // 获取当前节点的父节点
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentFiber(completedWork);
var next = void 0;
if ( (completedWork.mode & ProfileMode) === NoMode) {
next = completeWork(current, completedWork, renderLanes$1);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, renderLanes$1);
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentFiber();
if (next !== null) {
workInProgress = next;
return;
}
} else {
// 。。。。中间有部分代码省略
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
} else {
// We've unwound all the way to the root.
workInProgressRootExitStatus = RootDidNotComplete;
workInProgress = null;
return;
}
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber; // 指向父节点
workInProgress = completedWork;
} while (completedWork !== null); // 向上归到根节点
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
设立变量completedWork,以此判断是否递归完毕 循环执行completedWork函数
completedWork
主要做了什么?
completedWork根据不同节点类型,会做不同操作。其主要目的是生成/更新dom实例或组件实例
这里以hostComponent,也就是普通dom组件,
这里分为两种情况
这里判断是否存在current,和当前的stateNode,如果存在,代表是更新,否则就会重新生成dom实例
更新时,执行updateHostComponent$1更新fiber节点
updateHostComponent$1 = function (current, workInProgress, type, newProps) {
// 如果是存在alternate,代表是更新
// newProps 对应workInProgress.pengdingProps===workInProgress.memoizedProps,
// 用于存储react元素对应的children,clasaName等信息
var oldProps = current.memoizedProps; // 当前页面中节点对应的props对象
if (oldProps === newProps) {
// 如果相等,不做更新
return;
}
// 获取最新fiber节点的dom实例
var instance = workInProgress.stateNode;
//
var currentHostContext = getHostContext();
// 对比获取有哪些更新,结果为数组
var updatePayload = prepareUpdate(instance, type, oldProps, newProps, currentHostContext);
// 将更新队列放入当前workInProgress中
workInProgress.updateQueue = updatePayload;
if (updatePayload) {
markUpdate(workInProgress); // 更新fiber节点的flags标记,更新为4
}
};
当新建时,
通过createInstance创建当前节点的dom实例,createInstance中会调用createElement方法创建dom节点,并将创建的实例添加到虚拟dom树中
如果当前节点不是dom元素,比如是我们创建的组件,那是不会生成stateNode的;
如我们在页面中增加一个自定义组件
completedWork完成后,我们打印看下这个Test节点对应的fiber节点为
- completedWork执行完后,检测当前fiber节点是否存在兄弟节点,也就是sibling属性是否为null,如果存在,则改变workInProgress为兄弟节点,并return,循环结束,completeUnitOfWork执行结束,调用栈回到workLoopSync,workLoopSync中的循环继续,进入beginWork
3.如果不存在兄弟节点,则将completedWork设置为当前节点的父节点(return属性),递归继续,直到递归到根fiber节点,当根节点不存在retrun时,也就是不存在父节点时,workInProgress为null,workLoopSync循环结束,render阶段结束
总结
react在render阶段,主要是生成以页面根节点开始的fiber树,一个页面上只有一颗,从根元素开始向下递归遍历页面上的每一个元素,生成对应的fiber节点,子元素通过child连接(只存放第一个子元素,其余子元素放在child的sibling属性上),父节点通过 return属性链接,兄弟元素通过sibling属性访问,元素对应的dom实例则放在stateNode上,注意节点是函数不会生成stateNode。
graph TD
root --> App --> div --> header --> img & p & a
从根fiber节点出发,交替执行beginWork、completeWork,beginWork从app向下一层一层生成第一个子节点,并返回,直到末级,再逆向向上执行completeWork生成当前fiber节点的dom实例,如生成完毕后查看当前节点是否有兄弟节点,不存在,则回到父级节点的completeWork。有则执行第一个兄弟节点的beginWork,completeWork循环,直到所有兄弟节点循环完成,再向上追溯,直到页生成页面对应的完整fiber树,render阶段结束。
流程拆解如下
1.从根fiber节点出发,创建它的第一个子fiber节点 App,挂载到根fiber节点的child上,并返回
2.返回为App的fiber对象,再进入这个子fiber节点 APP,创建它的子fiber节点 div 并返回
3.进入div节点,创建并返回header节点
4.进入header节点,此时存在多个子节点 img p a ,创建第一个子节点img,其它节点存放在img节点的sibling属性上,返回img节点
5.进入img,执行completeWork,创建img的dom实例,并挂载到img节点的stateNode,挂载完成,不存在子节点,此时返回null,
6.查看img节点是否存在sibling,存在p,img阶段的completeWork结束
7.对p节点执行第5、6步,以此类推,直到a节点执行完成,回到header层(不存在兄弟元素),向上执行completeWork,直到root,此时生成完整fiber树