分析vue3源码6(diff算法)

152 阅读4分钟

component节点的处理

前言

上一节我们分析了diff算法对于不同类型节点的处理,本节我们就来分析最重要的component节点的处理。对于component节点的处理,要么mountComponent,要么updateComponent。

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          namespace,
          optimized,
        )
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          optimized,
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent 函数

主要流程

  1. 创建组件实例
// 兼容Vue 2.x的情况
const compatMountInstance =
  __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
  compatMountInstance ||
  (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense,
  ))
  1. 开发环境处理
if (__DEV__ && instance.type.__hmrId) {
  registerHMR(instance)
}

if (__DEV__) {
  pushWarningContext(initialVNode)
  startMeasure(instance, `mount`)
}
  1. keep-alive处理
// 注入渲染器内部方法到keep-alive组件
if (isKeepAlive(initialVNode)) {
  ;(instance.ctx as KeepAliveContext).renderer = internals
}
  1. 设置组件
// 非兼容模式下初始化组件
if (!(__COMPAT__ && compatMountInstance)) {
  if (__DEV__) {
    startMeasure(instance, `init`)
  }
  setupComponent(instance, false, optimized)
  if (__DEV__) {
    endMeasure(instance, `init`)
  }
}
  1. 异步组件处理
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
  // 避免HMR更新时的hydration
  if (__DEV__ && isHmrUpdating) initialVNode.el = null

  // 注册异步依赖
  parentSuspense &&
    parentSuspense.registerDep(instance, setupRenderEffect, optimized)

  // 创建占位符
  if (!initialVNode.el) {
    const placeholder = (instance.subTree = createVNode(Comment))
    processCommentNode(null, placeholder, container!, anchor)
  }
} else {
  // 设置渲染效果
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    namespace,
    optimized,
  )
}

关键步骤解析

  1. 创建组件实例

    • 首先检查是否是Vue 2.x兼容模式下预创建的实例
    • 如果不是,则通过createComponentInstance创建新实例
    • 实例中包含了组件的各种上下文信息
  2. 开发环境处理

    • 注册HMR(热更新)相关功能
    • 设置警告上下文
    • 添加性能测量
  3. keep-alive处理

    • 检查是否是keep-alive组件
    • 如果是,注入渲染器内部方法到组件上下文
  4. 设置组件(setupComponent)

    • 初始化props和slots
    • 执行组件的setup函数
    • 处理组件的状态和生命周期
    • 在开发环境下进行性能测量
  5. 异步组件处理

    • 检查组件是否有异步依赖(asyncDep)
    • 如果有异步依赖:
      • 在开发环境下处理HMR更新
      • 注册到父Suspense组件
      • 创建注释节点作为占位符
    • 如果没有异步依赖:
      • 直接设置渲染效果

updateComponent 函数

updateComponent 函数负责处理组件的更新逻辑。它接收以下参数:

  • n1: 旧的虚拟节点
  • n2: 新的虚拟节点
  • optimized: 是否优化

详细流程

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  // 获取组件实例并保持引用一致性
  const instance = (n2.component = n1.component)!

  // 判断是否需要更新
  if (shouldUpdateComponent(n1, n2, optimized)) {
    // 处理异步组件的特殊情况
    if (__FEATURE_SUSPENSE__ && instance.asyncDep && !instance.asyncResolved) {
      // 异步组件还未解析完成,只更新props和slots
      if (__DEV__) {
        pushWarningContext(n2)
      }
      updateComponentPreRender(instance, n2, optimized)
      if (__DEV__) {
        popWarningContext()
      }
      return
    } else {
      // 正常更新流程
      instance.next = n2     // 设置新的虚拟节点
      instance.update()      // 触发组件更新
    }
  } else {
    // 不需要更新,直接复制属性
    n2.el = n1.el           // 复用DOM元素
    instance.vnode = n2     // 更新vnode引用
  }
}

关键步骤解析

  1. 更新判断

    • 通过shouldUpdateComponent判断是否需要更新
    • 该函数会检查:
      • props是否发生变化
      • children是否发生变化
      • 组件是否有动态插槽
      • 是否有指令变化
  2. 异步组件处理

    • 检查组件是否是未解析的异步组件
    • 如果是未解析的异步组件:
      • 在开发环境下设置警告上下文
      • 只更新props和slots
      • 不触发完整的组件更新流程
  3. 正常更新流程

    • 设置新的虚拟节点(next)
    • 调用实例的update方法触发更新
    • update方法是一个响应式effect,会触发组件的重新渲染
  4. 优化处理

    • 当不需要更新时:
      • 直接复用原有的DOM元素(el)
      • 更新组件实例的vnode引用
    • 这种优化可以避免不必要的DOM操作

两个函数的关系

  1. 调用时机

    • mountComponent: 组件首次渲染时调用
    • updateComponent: 组件数据变化需要更新时调用
  2. 职责区分

    • mountComponent:
      • 负责组件的初始化
      • 创建组件实例
      • 设置响应式系统
      • 首次渲染DOM
    • updateComponent:
      • 负责组件的更新
      • 判断是否需要更新
      • 处理异步组件更新
      • 触发重新渲染

总结

本节,我们分析了Vue组件系统中负责组件挂载和更新的两个核心函数。在这两个函数执行的过程中又调用了一些重要的内部函数,待进一步分析:

  1. setupComponent: 负责组件的初始化设置
  2. setupRenderEffect: 创建组件的渲染副作用
  3. shouldUpdateComponent: 判断组件是否需要更新
  4. updateComponentPreRender: 处理异步组件的预渲染更新

下一节我们分析setupComponent函数和setupRenderEffect函数。