Vue3读源码系列(三):组件的挂载与更新

426 阅读8分钟

根组件挂载的章节我们说到创建了根组件vnode后,我们去执行patch操作,挂载组件,下面我们就进入到patch,看看究竟干了什么

patch

// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
  n1, // 旧vnode
  n2, // 新vnode
  container, // 容器 container._vnode === n1
  anchor = null, // 锚点
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
  if (n1 === n2) {
    return
  }
  // patching & not same type, unmount old tree
  // 如果新旧节点类型不同,卸载旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    unmount(n1, parentComponent, parentSuspense, true)
    n1 = null
  }
  // 使用手动编写的渲染函数,应始终完全进行差分 不使用dynamicChildren
  if (n2.patchFlag === PatchFlags.BAIL) {
    optimized = false
    n2.dynamicChildren = null
  }
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
    case Comment: // 注释
      processCommentNode(n1, n2, container, anchor)
      break
    case Static:
      if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment:
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // element类型
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // component类型
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        ...
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        ...
  }
  ...
}

进入patch,会对type进行一个判断,根绝类型来决定对应的处理,我们这里传入的是根组件的vnode,所以type就是根组件的组件描述对象,所以会执行processComponent函数。

processComponent

// packages/runtime-core/src/renderer.ts
const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    // 判断旧节点是否为null 为null则执行挂载操作 否则执行更新操作
    if (n1 == null) {
      // 是否缓存
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 挂载组件
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent

// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 2.x 兼容vue2
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
    // 创建组件实例 createComponentInstance执行
    const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    ...

    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      if (__DEV__) {
        startMeasure(instance, `init`)
      }
      // 初始化组件 处理setup的两个参数 执行setup 生成render函数
      //(所以setup是在所有选项式API钩子之前调用 包括beforeCreate)
      setupComponent(instance)
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }

    ...
    // 1 创建一个组件更新函数
    //  1.1 render获得vnode
    //  1.2 patch(oldVnode, newVnode)
    // 2 创建更新机制 new ReactiveEffect(更新函数)
    // 执行渲染副作用函数
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

进入mountComponent后首先会创建组件实例,然后去执行setupComponent,初始化一些数据,再下面就是创建函数的更新机制,这里是组件能响应式更新的核心。我们先看setupComponent做了什么:

setupComponent

// packages/runtime-core/src/component.ts
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR
  // vnode的props和子元素
  const { props, children } = instance.vnode
  // 是否是有状态的组件
  const isStateful = isStatefulComponent(instance)
  // 初始化props
  initProps(instance, props, isStateful, isSSR)
  // 初始化slots
  initSlots(instance, children)
  // 执行setupStatefulComponent获取setupResult
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR) // 执行setup
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

可以看到setupComponent会初始化props和slots,然后执行setupStatefulComponent,这里主要是执行setup函数,并返回结果

setupStatefulComponent
// packages/runtime-core/src/component.ts
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 组件描述对象
  const Component = instance.type as ComponentOptions
  ...
  // 0. create render proxy property access cache
  // 创建render proxy属性访问缓存 作用是缓存访问过的属性
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  // 代理ctx,拦截ctx的属性访问 从而实现取值的优先级:setupState > data > props > ctx
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    // 创建setupContext
    const setupContext = (instance.setupContext =
      // setup参数个数判断 大于一个参数创建setupContext
      setup.length > 1 ? createSetupContext(instance) : null)
    // instance赋值给currentInstance
    // 设置当前实例为instance 为了在setup中可以通过getCurrentInstance获取到当前实例
    // 同时开启instance.scope.on()
    setCurrentInstance(instance)
    // 暂停tracking 暂停收集副作用函数
    pauseTracking()
    // 执行setup
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      // setup参数
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    // 重新开启副作用收集
    resetTracking()
    // currentInstance置为空 
    // activeEffectScope赋值为instance.scope.parent
    // 同时instance.scope.off()
    unsetCurrentInstance()

    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
      if (isSSR) {
        ...
      } else if (__FEATURE_SUSPENSE__) {
        ...
      } else if (__DEV__) {
        ...
      }
    } else {
      // 处理setupResult 如果为函数则会作用组件的render函数 否则赋值给instance.setupState
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    // 处理options API 兼容vue2
    finishComponentSetup(instance, isSSR)
  }
}

上述函数主要是执行setup函数,还会执行finishComponentSetup,处理options API, 其实从这里我们还能看出setup和其他options生命周期的执行顺序,先执行的setup,然后才去处理options钩子函数。
处理完setup,就该去执行setupRenderEffect了

setupRenderEffect

// packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 组件挂载/更新函数
    const componentUpdateFn = () => {
      ...
    }
    // create reactive effect for rendering
    // 创建effect
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update), // scheduler
      instance.scope // 将effect添加到组件的scope.effects中
    ))
    // 组件更新函数
    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true)

    if (__DEV__) {
     ...
    }
    // 首次执行组件更新
    update()
  }

该函数只做了一件事,创建了一个effect,然后执行update(),最终会执行componentUpdateFn函数。componentUpdateFn中会执行render函数和patch操作,执行render函数的过程中会建立响应式数据和effect的关系,以后只要响应式数据发生变化就会再次执行componentUpdateFn函数已达到组件更新。下面具体看看componentUpdateFn做了什么。PS:这里不熟悉响应式的小伙伴可能会有点懵,但是不要着急,以后章节会讲到底怎么实现的响应式,响应式数据到底怎么收集到的effect对象。

componentUpdateFn

为了方便阅读只放了比较核心的代码:

const componentUpdateFn = () => {
  // 判断组件是否已经挂载
  if (!instance.isMounted) {
    let vnodeHook: VNodeHook | null | undefined
    const { el, props } = initialVNode
    // 生命周期和父instance
    const { bm, m, parent } = instance
    const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
    toggleRecurse(instance, false)
    // beforeMount hook
    if (bm) {
      invokeArrayFns(bm)
    }
    // onVnodeBeforeMount
    ...
    if (el && hydrateNode) {
      // ssr 相关
    } else {
      if (__DEV__) {
        startMeasure(instance, `render`)
      }
      // 执行render函数获得subTree(也是一个vnode) 讲subTree挂载到instance上 以供更新使用
      const subTree = (instance.subTree = renderComponentRoot(instance))
      ...
      // patch subTree初次挂载
      patch(
        null,
        subTree,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG
      )
      if (__DEV__) {
        endMeasure(instance, `patch`)
      }
      // el同步到initialVNode
      initialVNode.el = subTree.el
    }
    // mounted hook
    if (m) {
      queuePostRenderEffect(m, parentSuspense)
    }
    // onVnodeMounted
    ...
    // 组件已经挂载
    instance.isMounted = true

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      devtoolsComponentAdded(instance)
    }

    // #2458: deference mount-only object parameters to prevent memleaks
    initialVNode = container = anchor = null as any
  } else {
    // 组件更新
    let { next, bu, u, parent, vnode } = instance
    let originNext = next
    let vnodeHook: VNodeHook | null | undefined
    ...
    // 执行render函数获得nextTree
    const nextTree = renderComponentRoot(instance)
    if (__DEV__) {
      endMeasure(instance, `render`)
    }
    // 获取老的subTree
    const prevTree = instance.subTree
    instance.subTree = nextTree

    if (__DEV__) {
      startMeasure(instance, `patch`)
    }
    // patch新旧节点更新组件
    patch(
      prevTree,
      nextTree,
      // parent may have changed if it's in a teleport
      hostParentNode(prevTree.el!)!,
      // anchor may have changed if it's in a fragment
      getNextHostNode(prevTree),
      instance,
      parentSuspense,
      isSVG
    )
    ...
  }
}

这里的patch操作其实就是调用本章开头的那个patch,可以看到patch其实是一个递归操作,这里patch subtree如果根组件的根元素是组件则会继续执行processComponent,如果是一个element元素则会执行processElement,processElement中会处理children,又会调用patch,如此递归直到整个组件挂载完成。

总结

  1. patch(n1, n2):组件类型的vnode匹配到processComponent,处理组件的挂载或更新
  2. processComponent:此时组件还未挂载,所以n1为null,执行mountComponent挂载组件
  3. mountComponent:首先创建组件实例;然后执行setupComponent初始化组件,setupComponent中会先执行setup,然后处理options API(options API的处理顺序是:props:props在setup执行前就已经处理过了 -> inject -> methods -> data -> computed -> watch),处理options API之前会执行beforeCreate生命周期,处理完options API后会执行created生命周期;然后执行setupRenderEffect,这里就会创建一个组件更新函数componentUpdateFn,通过new ReactiveEffect传入componentUpdateFn创建出该组件的effect实例,然后执行组件更新函数,componentUpdateFn中会判断isMounted为false,然后生成subTree(这里就会通过track方法去收集对应的effect),然后patch(null, subTree),beforeMount和mounted生命周期会在这个过程前后执行。
  4. 父子组件更新:当某个响应式变量发生改变,trigger收集的effects被拿出来执行其scheduler或run方法,flushJobs时会对effects进行排序(父组件排到前面执行),所以先执行父组件的组件更新函数,在组件更新函数中会生成新的subtree和老的subtree进行patch操作,在path过程中遇到子组件,先判断子组件是否需要更新,这里只考虑props的情况下会进行浅层比较,如果需要更新则执行子组件的更新函数(这里会移除queue中的子组件effect防止重复执行)。这里符合深度优先的更新策略。这里值得注意的是父子updated钩子的调用顺序不是固定的:假设现在一个响应式数据的变化(这里的变化特指一个reactive对象的某个属性变化)将执行父子组件组件更新函数,执行父组件的patch到检查子组件是否需要更新时,因为是浅层比较props所以结果是true,这就意味着父组件认为子组件不需要更新,直到父组件更新完将updated放到对应的queue中,父组件的更新函数执行完后轮到子组件的更新函数执行,完成后放到queue中,这种情况下就导致父组件的updated钩子函数会先执行。除了这种情况,如果在父组件的patch过程中通过props的浅层比较判断子组件需要更新,这种情况下就是子组件的updated钩子先执行。