本篇我们看下协调器reconciler部分,本系列文章旨在分析react核心模块内部运行原理和源码实现,提升架构和编码能力。
一、协调器在react内部运行部分
协调器reconciler工作部分在react内部被称为render阶段,即class组件的render函数、函数组件本身的调用等,根据Scheduler调度结果的不同,render阶段可能开始于开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新。
这两个方法会分别调用如下方法,可以看到,他们唯一的区别是是否调用shouldYield
。如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历,也就是说的并发模式可中断。
workInProgress
代表当前已创建的workInProgress fiber
。performUnitOfWork整个阶段最核心的部分,被称为工作单元,构建fiber树,每个fiber节点的工作可以分为两部分:"递"和"归"。 这里是最核心的部分。
js
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
二、协调器整体工作流程
- 调用入口: 提供
scheduleUpdateOnFiber
函数, 在mount阶段或者update阶段去调用。 - 注册调度任务: 与调度中心(
scheduler
包)交互, 注册调度任务task
, 等待任务回调. - 执行任务回调: 在内存中构造出
fiber树
, 会调用渲染器(react-dom
)的一些接口创建dom节点, 在内存中创建出与fiber
对应的DOM
节点. - 输出: 带有副作用标记的fiber树,供Commit阶段使用。
调用入口
在react-reconciler
对外暴露的 api 函数中, 只要涉及到需要改变 fiber 的操作(无论是首次渲染
或后续更新
操作), 详细见前面文章react工作两大循环里面的图,很详细的标出来首次渲染的时候updateContainer->scheduleUpdateOnFiber阶段。 最后都会间接调用scheduleUpdateOnFiber
, 所以scheduleUpdateOnFiber
函数是输入链路中的必经之路
scss
// 首次渲染
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...省略与逻辑不相关代码
// 创建update
const update = createUpdate(eventTime, lane, suspenseConfig);
// update.payload为需要挂载在根节点的组件
update.payload = {element};
// callback为ReactDOM.render的第三个参数 ------ 回调函数
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将生成的update加入updateQueue
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);
// ...省略与逻辑不相关代码
}
//再次更新阶段
function dispatchAction(fiber, queue, action) {
// ...创建update
var update = {
....
next: null,
};
// ...将update加入queue.pending
var alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber$1 ||
(alternate !== null && alternate === currentlyRenderingFiber$1)
) {
// render阶段触发的更新
didScheduleRenderPhaseUpdateDuringThisPass =
didScheduleRenderPhaseUpdate = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// ...fiber的updateQueue为空,优化路径
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
// scheduleUpdateOnFiber统一入口函数
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ... 省略部分无关代码
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// 直接进行`fiber构造`
performSyncWorkOnRoot(root);
} else {
// 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
ensureRootIsScheduled(root, eventTime);
}
} else {
// 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
ensureRootIsScheduled(root, eventTime);
}
}
逻辑进入到scheduleUpdateOnFiber
之后, 后面有 2 种可能:
- 不经过调度, 直接进行
fiber构造
. - 注册调度任务, 经过
Scheduler
包的调度, 间接进行fiber构造
.
注册调度任务
scheduleUpdateOnFiber
函数之后, 立即进入ensureRootIsScheduled
函数调用
ini
// ... 省略部分无关代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
const newCallbackPriority = returnNextLanesPriority();
if (nextLanes === NoLanes) {
return;
}
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
cancelCallback(existingCallbackNode);
}
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
}else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
ensureRootIsScheduled
的逻辑很清晰, 分为 2 部分:
-
判断是否需要注册新的调度(如果无需新的调度, 会退出函数)
-
根据任务优先级,判断同步任务直接调用scheduleSyncCallback,会进行微任务调度,在微任务中遍历执行performSyncWorkOnRoot。如果是其他优先级,会进入scheduleCallback,这个函数就是上一节调度器Scheduler中的unstable_scheduleCallback,这里就是去调度器的任务队列中注册一个新的调度任务,然后时间过去后去调度执行,也就是会执行
performConcurrentWorkOnRoot
。
执行任务回调
任务回调, 实际上就是执行performSyncWorkOnRoot
或performConcurrentWorkOnRoot
函数,即内部执行performUnitOfWork,两个部分beginWork"递"和completeWork"归"两个子阶段。
- beginWork阶段
首先判断当前流程市属于mount阶段还是update阶段,判断的依据就是current === null(fiber是双缓存构架,首次渲染,不存在当前组件对应的Fiber节点
在上一次更新时的Fiber节点
,即mount
时current === null
),即mount
时current === null
。组件update
时,由于之前已经mount
过,所以current !== null
。
js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
....
}
// 复用current
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
上面可以看出,mount阶段当不满足优化路径时,就会跳过上面的if逻辑,直接进入新建子Fiber
。我们可以看到,根据fiber.tag
不同,进入不同类型Fiber
的创建逻辑。 update阶段时我们可以看到,满足如下情况时didReceiveUpdate === false
(即可以直接复用前一次更新的子Fiber
,不需要新建子Fiber
) 对于我们常见的组件类型,如(FunctionComponent
/ClassComponent
/HostComponent
),最终会进入reconcileChildren方法,如下:
js
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
...
return reconcileChildFibers;
}
上面mountChildFibers
与reconcileChildFibers
这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers
会为生成的Fiber节点
带上effectTag
属性,而mountChildFibers
不会。
- 对于
mount
的组件,他会创建新的子Fiber节点
- 对于
update
的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点
比较,就是Diff
算法,将比较的结果生成新Fiber节点
。这里面的副作用包含插入、删除、移动,不包含更新。更新是在completeWork阶段做的。
ChildReconciler函数传递shouldTrackSideEffects标识, 是否为Fiber对象添加 effectTag,对于初始渲染来说, 只有根组件需要添加, 其他元素不需要添加, 防止过多的 DOM操作(这是一个优化)。
- completeWork阶段
与beginWork阶段类似,completeWork阶段也会根据wip.tag区分对待,进入不同的逻辑处理
js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
completeWork阶段,主要是创建或者标记元素更新和flags冒泡。
mount时
- 为
Fiber节点
生成对应的DOM节点
- 将子孙
DOM节点
插入刚生成的DOM节点
中 - 与
update
逻辑中的updateHostComponent
类似的处理props
的过程 - 执行bubbleProperties完成flags冒泡
update时
onClick
、onChange
等回调函数的注册- 处理
style prop
- 处理
DANGEROUSLY_SET_INNER_HTML prop
- 处理
children prop
- 执行bubbleProperties完成flags冒泡
三、总结
我们了解了Reconciler的工作流程,它主要是采用DFS的顺序构建fiber树,主要是可以划分为beginWork和completeWork阶段,beginWork会根据当前fiberNode创建下一级fiberNode,在update时通过diff算法标记Placement(新增、移动),ChildDeletion(删除)。completeWork在mount阶段会构建DOM Tree,在update时标记Update(属性更新),最终执行flags冒泡。当最终HostRootFiber完成completeWork时,Reconciler的工作流程结束,此时我们得到了一颗带flags的wip Fiber tree会传递给commit阶段去消费,完成页面的渲染。