工作循环 WorkLoop
在工作循环中,将会执行一个 while
语句,每执行一次循环,都会完成对一个 fiber
节点的处理。在 workLoop 模块中有一个指针 workInProgress 指向当前正在处理的 fiber ,它会不断向链表的尾部移动,直到指向的值为 null ,就停止这部分工作, workLoop 的部分也就结束了。
每处理一个 fiber 节点都是一个工作单元,结束了一个工作单元后 React 会进行一次判断,是否需要暂停工作检查有没有更高优先级的用户交互进来。
function workLoopConcurrent() {
// 执行工作直到 Scheduler 要求我们 yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
跳出条件只有:
- 所有 fiber 都已经被遍历结束了
- 当前线程的使用权移交给了外部任务队列
但是我们现在讨论的是第一次渲染,触屏渲染的优先级高于一切,所以并不存在第二个限制条件。
function workLoopSync () {
// 只要没有完成 reconcile 就一直执行
while(workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress as Fiber)
}
}
PerformUnitOfWork & beginWork
单元工作 performUnitOfWork 的主要工作是通过 beginWork 来完成。 beginWork 的核心工作是通过判断 fiber.tag 判断当前的 fiber 代表的是一个类组件、函数组件还是原生组件,并且针对它们做一些特殊处理。这一切都是为了最终步骤:操作真实 DOM 做准备,即通过改变 fiber.effectTag 和 pendingProps 告诉后面的 commitRoot 函数应该对真实 DOM 进行怎样的改写。
switch (workInProgress.tag) {
// RootFiber
case WorkTag.HostRoot:
return updateHostRoot(current as Fiber, workInProgress, renderExpirationTime)
// class 组件
case WorkTag.ClassComponent: {
const Component = workInProgress.type
const resolvedProps = workInProgress.pendingProps
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
)
}
...
}
此处就以 Class 组件为例,查看一下具体是如何构建的。
之前有提过,对于类组件而言, fiber.stateNode 会指向这个类之前构造过的实例。
// 更新 Class 组件
function updateClassComponent (
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpiration: number
) {
// 如果这个 class 组件被渲染过,stateNode 会指向类实例
// 否则 stateNode 指向 null
const instance = workInProgress.stateNode
if (instance === null) {
// 如果没有构造过类实例
...
} else {
// 如果构造过类实例
...
}
// 完成 render 的构建,将得到的 react 元素和已有元素进行调和
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
false,
renderExpiration
)
return nextUnitOfWork
如果这个 fiber 并没有构建过类实例的话,就会调用它的构建函数,并且将更新器 updater 挂载到这个类实例上。(处理 setState 逻辑用的,事实上所有的类组件实例上的更新器都是同一个对象,后面会提到)
if (instance === null) {
// 这个 class 第一次渲染
if (current !== null) {
// 删除 current 和 WIP 之间的指针
current.alternate = null
workInProgress.alternate = null
// 插入操作
workInProgress.effectTag |= EffectTag.Placement
}
// 调用构造函数,创造新的类实例
// 给予类实例的某个指针指向更新器 updater
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpiration
)
// 将属性挂载到类实例上,并且触发多个生命周期
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpiration
)
}
如果实例已经存在,就需要对比新旧 props 和 state ,判断是否需要更新组件(万一写了 shouldComponentUpdate 呢)。并且触发一些更新时的生命周期钩子,例如 getDerivedStateFromProps 等等。
else {
// 已经 render 过了,更新
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpiration
)
}
属性计算完毕后,调用类的 render
函数获取最终的 ReactElement ,打上 Performed 标记,代表这个类在本次渲染中已经执行过了。
// 完成 Class 组件的构建
function finishClassComponent (
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderExpiration: number
) {
// 错误 边界捕获
const didCaptureError = false
if (!shouldUpdate && !didCaptureError) {
if (hasContext) {
// 抛出问题
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpiration
)
}
}
// 实例
const instance = workInProgress.stateNode
let nextChildren
nextChildren = instance.render()
// 标记为已完成
workInProgress.effectTag |= EffectTag.PerformedWork
// 开始调和 reconcile
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpiration
)
return workInProgress.child
}
调和过程
如果还记得之前的内容的话,我们在一切工作开始之前只是构建了第一个根节点 fiberRoot 和第一个无意义的空 root ,而在单个元素的调和过程 reconcileSingleElement 中会根据之前 render 得到的 ReactElement 元素构建出对应的 fiber 并且插入到整个 fiber 链表中去。
并且通过 placeSingleChild 给这个 fiber 的 effectTag 打上 Placement 的标签,拥有 Placement 标记后这里的工作就完成了,可以将 fiber 指针移动到下一个节点了。
// 处理对象类型(单个节点)
const isObjectType = isObject(newChild) && !isNull(newChild)
// 对象
if (isObjectType) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 在递归调和结束,向上回溯的过程中
// 给这个 fiber 节点打上 Placement 的 Tag
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
)
}
// 还有 Fragment 等类型
}
}
// 如果这时子元素是字符串或者数字,按照文字节点来处理
// 值得一提的是,如果元素的子元素是纯文字节点
// 那么这些文字不会被转换成 fiber
// 而是作为父元素的 prop 来处理
if (isString(newChild) || isNumber(newChild)) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
expirationTime
)
)
}
// 数组
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
}
文章篇幅有限,对于函数组件和原生组件这里就不做过多介绍。假设我们已经完成了对于所有 WIP 的构建和调和过程,对于第一次构建而言,我们需要插入大量的 DOM 结构,但是到现在我们得到的仍然是一些虚拟的 fiber 节点。
所以,在最后一次单元工作 performUnitOfWork 中将会执行 completeWork
,在此之前,我们的单元工作是一步步向尾部的 fiber 节点移动。而在 completeWork
中,我们的工作将是自底向上,根据 fiber 生成真实的 dom 结构,并且在向上的过程中将这些结构拼接成一棵 dom 树。
export function completeWork (
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: number
): Fiber | null {
// 最新的 props
const newProps = workInProgress.pendingProps
switch (workInProgress.tag) {
...
case WorkTag.HostComponent: {
// pop 该 fiber 对应的上下文
popHostContext(workInProgress)
// 获取 stack 中的当前 dom
const rootContainerInstance = getRootHostContainer()
// 原生组件类型
const type = workInProgress.type
if (current !== null && workInProgress.stateNode !== null) {
// 如果不是初次渲染了,可以尝试对已有的 dom 节点进行更新复用
updateHostComponent(
current,
workInProgress,
type as string,
newProps,
rootContainerInstance
)
} else {
if (!newProps) {
throw new Error('如果没有 newProps,是不合法的')
}
const currentHostContext = getHostContext()
// 创建原生组件
let instance = createInstance(
type as string,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
)
// 将之前所有已经生成的子 dom 元素装载到 instance 实例中
// 逐步拼接成一颗 dom 树
appendAllChildren(instance, workInProgress, false, false)
// fiber 的 stateNode 指向这个 dom 结构
workInProgress.stateNode = instance
// feat: 这个函数真的藏得很隐蔽,我不知道这些人是怎么能注释都不提一句的呢→_→
// finalizeInitialChildren 作用是将 props 中的属性挂载到真实的 dom 元素中去,结果作为一个判断条件被调用
// 返回一个 bool 值,代表是否需要 auto focus(input, textarea...)
if (finalizeInitialChildren(instance, type as string, newProps, rootContainerInstance, currentHostContext)) {
markUpdate(workInProgress)
}
}
}
}
return null
}
构建完毕后,我们得到了形如下图,虚拟 dom 和 真实 dom,父元素和子元素之间的关系结构
截止到当前,调和 reconcile 工作已经完成,我们已经进入了准备提交到文档 ready to commit 的状态。其实从进入 completeUnitOfWork 构建开始,后面的过程就已经和时间片,任务调度系统没有关系了,此时一切事件、交互、异步任务都将屏气凝神,聆听接下来 dom 的改变。
// 提交根实例(dom) 到浏览器真实容器 root 中
function commitRootImpl (
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel
) {
...
// 因为这次是整个组件树被挂载,所以根 fiber 节点将会作为 fiberRoot 的 finishedWork
const finishedWork = root.finishedWork
...
// effect 链表,即那些将要被插入的原生组件 fiber
let firstEffect = finishedWork.firstEffect
...
let nextEffect = firstEffect
while (nextEffect !== null) {
try {
commitMutationEffects(root, renderPriorityLevel)
} catch(err) {
throw new Error(err)
}
}
}
在 commitMutationEffects 函数之前其实对 effect 链表还进行了另外两次遍历,分别是一些生命周期的处理,例如 getSnapshotBeforeUpdate ,以及一些变量的准备。
// 真正改写文档中 dom 的函数
// 提交 fiber effect
function commitMutationEffects (
root: FiberRoot,
renderPriorityLevel: number
) {
// @question 这个 while 语句似乎是多余的 = =
while (nextEffect !== null) {
// 当前 fiber 的 tag
const effectTag = nextEffect.effectTag
// 下方的 switch 语句只处理 Placement,Deletion 和 Update
const primaryEffectTag = effectTag & (
EffectTag.Placement |
EffectTag.Update |
EffectTag.Deletion |
EffectTag.Hydrating
)
switch (primaryEffectTag) {
case EffectTag.Placement: {
// 执行插入
commitPlacement(nextEffect)
// effectTag 完成实名制后,要将对应的 effect 去除
nextEffect.effectTag &= ~EffectTag.Placement
}
case EffectTag.Update: {
// 更新现有的 dom 组件
const current = nextEffect.alternate
commitWork(current, nextEffect)
}
}
nextEffect = nextEffect.nextEffect
}
}
截至此刻,第一次渲染的内容已经在屏幕上出现。也就是说,真实 DOM 中的内容不再对应此时的 current fiber ,而是对应着我们操作的 workInProgress fiber ,即函数中的 finishedWork 变量。
// 在 commit Mutation 阶段之后,workInProgress tree 已经是真实 Dom 对应的树了
// 所以之前的 tree 仍然是 componentWillUnmount 阶段的状态
// 所以此时, workInProgress 代替了 current 成为了新的 current
root.current = finishedWork
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论