发布于

Vue3的组件的更新过程

Authors
  • avatar
    Name
    田中原
    Twitter

Vue3的组件的更新过程

目录

渲染的入口

接着上次挂载组件往下看packages/runtime-core/src/renderer.tsmountComponent 组件渲染实际上是调用了setupRenderEffect

setupRenderEffect 主要逻辑

  1. 声明了一个组件更新函数,
    1. vnode的创建
    2. 新旧vnode通过patch
  2. 处理副作用,和SchedulerJob
    1. 通过new ReactiveEffect 创建effect 实例,
    2. 实例的update函数加入effect.run
  3. 最后调用update

节点更新在patch 中的逻辑

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    namespace = undefined,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
  ) => {
    if (n1 === n2) {
      // 新旧相同直接返回
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      // n1有值是更新,并且不是相同类型,卸载旧树。如果更新不走这里就该走diff的流程
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      // n1改为null,后续会重新挂载
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }
    ...
   if (shapeFlag & ShapeFlags.ELEMENT) {
     // 处理成真实的元素
      processElement()
    } else if (shapeFlag & ShapeFlags.COMPONENT) {
     // 对组件处理
      processComponent()
  }

更新时对组件的处理

processComponentupdateComponent

主要判断当前组件的子组件是否需要更新 如果子组件也需要更新(子组件自己状态变化了),会从更新队列中删除子组件,手动更新子

  /**
   * @description 判断子组件是否需要更新
   * 如果需要则递归执行子组件的副作用渲染函数更新
   * 否则只更新vnode属性,让子组件实例保留对组件vnode的引用
   * 用于子组件自身数据变化重新渲染时,渲染函数能拿到父组件的vnode
   */
  const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
    const instance = (n2.component = n1.component)!

    // 根据新旧子组件的VNode,判断是否需要更新子组件
    if (shouldUpdateComponent(n1, n2, optimized)) {
      if (
        __FEATURE_SUSPENSE__ &&
        instance.asyncDep &&
        !instance.asyncResolved
      ) {
        // async & still pending(suspense未resolved) - 只更新props和slots,因为组件的渲染响应效果尚未设置
        // 更新组件上的信息,比如props、slots等
        updateComponentPreRender(instance, n2, optimized)
        return
      } else {
        instance.next = n2
        // 如果子组件也在队列中,删除它,以避免在同一次刷新中多次更新同一个子组件。
        // 避免子组件由于自身数据变化导致的重复更新。等于是把子组件的更新任务从队列中移除,手动更新了子组件
        invalidateJob(instance.update)
        // instance.update是响应式effect
        instance.effect.dirty = true
        // 执行子组件的副作用渲染函数,主动触发子组件的更新
        instance.update()
      }
    } else {
      // 不需要更新,只需复制属性,
      n2.el = n1.el
      instance.vnode = n2
    }
  }

对元素的处理

patchElementpatchChildren

最终的dom操作

  1. 通过patchChildren 处理子节点,主要是文本、数组、或无子节点
  2. 通过patchProps 处理dom上的属性
/**
   *
   * @description 主要做两件事
   * 1. patchProps处理props
   * 2. patchChildren处理子节点
   */
  const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    const el = (n2.el = n1.el!)
    let { patchFlag, dynamicChildren, dirs } = n2
    patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
    const oldProps = n1.props || EMPTY_OBJ
    const newProps = n2.props || EMPTY_OBJ
    let vnodeHook: VNodeHook | undefined | null

    // 有父组件的话,禁用递归
    parentComponent && toggleRecurse(parentComponent, false)
    if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
      // 有vnodeHook的话,调用vVnode的onVnodeBeforeUpdate
      invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
    }
    if (dirs) {
      // dirs,调用的beforeUpdate生命周期
      invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
    }
    parentComponent && toggleRecurse(parentComponent, true)

    ...
    // 没法优化、全量diff
    patchChildren()
    ... 省略成吨的逻辑判断
    patchProps()

    if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
      // dom处理完毕调用updated生命周期
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
      }, parentSuspense)
    }
  }