返回介绍

工作循环 WorkLoop

发布于 2025-01-10 12:49:04 字数 9998 浏览 0 评论 0 收藏 0

在工作循环中,将会执行一个 while 语句,每执行一次循环,都会完成对一个 fiber 节点的处理。在 workLoop 模块中有一个指针 workInProgress 指向当前正在处理的 fiber ,它会不断向链表的尾部移动,直到指向的值为 null ,就停止这部分工作, workLoop 的部分也就结束了。

每处理一个 fiber 节点都是一个工作单元,结束了一个工作单元后 React 会进行一次判断,是否需要暂停工作检查有没有更高优先级的用户交互进来。

function workLoopConcurrent() {
  // 执行工作直到 Scheduler 要求我们 yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

跳出条件只有:

  1. 所有 fiber 都已经被遍历结束了
  2. 当前线程的使用权移交给了外部任务队列

但是我们现在讨论的是第一次渲染,触屏渲染的优先级高于一切,所以并不存在第二个限制条件。

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,父元素和子元素之间的关系结构

http://www.wenjiangs.com/wp-content/uploads/2024/docimg16/1004-cyavcuw2eke.png

截止到当前,调和 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文