Vue3大侠修炼手册4- 初始化流程分析(2)

「这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战

Vue3大侠修炼手册4- 初始化流程分析(2)

题记

上篇文章我们分析了createApp的app实例构建过程,同时简要分析了mount的流程。今天这篇文章,我们书接上文,更加深入的窥探一下mount的流程。

再看mount

在上文中,我们在todomvc.html中把断点放到mount处来查看mount的流程。 Pasted image 20220207225549.png 然后根据代码单步调试,我们知道了其虽然进行了封装,但最终核心部分仍是调用了 apiCreateApp.ts 文件下的 createAppAPI函数所返回的对象中的mount方法。 而该方法主要做了两件事

  • 调用createVNode方法,创建vnode

  • 调用render(vnode, rootContainer, isSVG)方法,把vnode转化为真实dom,然后绑定到rootContainer上。

关于createVNode部分,我们后面会专门讲解,今天这篇文章,我们着重分析,这个render函数所做的事情。

Pasted image 20220207230959.png 我们从上一节代码分析可知,此处的render函数,是在baseCreateRenderer定义并返回的 render函数。

render函数

这段函数不长,我把它复制下来:

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
		// 核心执行流程
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
复制代码

我们可以看到这个函数里,由于我们上面刚更新过vnode的值,所以函数内部走else流程,执行了一个名为patch的函数, 当我们进入其中,可以发现由于首次container._vnode为空, 我们走了挂载流程,patch函数就是我们要找的核心更新/挂载函数。

在这里我去除其中的边角逻辑,只展示核心的判断逻辑.

patch函数

源码片段:

// packages/runtime-core/src/renderer.ts 中 patch函数
// 删除了边角逻辑等,方便大家阅读,若要看全部内容,请移步源码
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    const { type, ref, shapeFlag } = n2
    switch (type) { // 根据type执行对应的流程
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        // ... mountStaticNode() or patchStaticNode()
        break
      case Fragment:
        // ... processFragment()
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // !! 首次进入会进入processComponent
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // ...
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          // ...
        } else if (__DEV__) {
          // ...
        }
    }
  }
复制代码

断点截图: Pasted image 20220208144734.png 通过单步断点我们可以看到首次的type 为一个对象,shapeFlag4,所以会走switchdefault流程。而通过&位运算符的执行结果和断点执行我们可以知道,首次会执行processComponent函数。

processComponent函数

processComponent源码片段

  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
    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)
    }
  }


复制代码

根据上面的代码我们可以知道此时n1 == null所以我们的代码会走mountComponent流程,是挂载流程,而非updateComponent流程。

我们继续深入mountComponent代码中看看里面究竟做了哪些事情

mountComponent

mountComponent精简源码

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
	// 创建组件实例
    const instance: ComponentInternalInstance = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))
    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      // 组件的初始化
      setupComponent(instance)
    }
    // 执行副作用,在setupComponent时已经确保有了render函数
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }
复制代码

重上图我精简后的代码我们可以看到mountComponent主要做了三件事,分别调用了三个函数

  1. 创建组件实例 - createComponentInstance
  2. 进行组件的初始化 - setupComponent
  3. 执行组件渲染副作用函数 -setupRenderEffect

关于第一步创建实例,我们今天先不讨论,感兴趣的同学可以自行进入调试学习。 我们主要看2、3步。

我们先来看第二步

setupComponent函数

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  // 初始化 props和slots
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  // 初始化组件
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
复制代码

我们可以看到setupComponent函数除初始化propsslots以外,还执行了setupStatefulComponent. 那我们再浏览一下这个 setupStatefulComponent函数

setupStatefulComponent

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // 0. create render proxy property access cache
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    setCurrentInstance(instance)
    pauseTracking()
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    unsetCurrentInstance()

    handleSetupResult(instance, setupResult, isSSR)
  } else {
    finishComponentSetup(instance, isSSR)
  }
}
复制代码

我们不去看目前我们不关系的逻辑,不然会陷入无尽的细节,而走不出我们的流程。我们主要最后这个if...else逻辑的最后三行代码,而进入handleSetupResult函数我们进入会发现,最终handleSetupResult还是会调用finishComponentSetup这个函数。

而这个函数目前来说主要的目的就是判断我们的instance是否已经有render函数,如果没有,会帮助其设置一个render函数,这个render函数的功能就是把我们的模板函数转换为vnode。在这里我们先不深究,我把精简后的代码贴到下面。

finishComponentSetup 精简代码
export function finishComponentSetup(
  instance: ComponentInternalInstance
) {
  const Component = instance.type as ComponentOptions
  // 处理 instance 的 render
  if (!instance.render) {
    // only do on-the-fly compile exist
    if (compile && !Component.render) {
      const template = Component.template
      if (template) {
        Component.render = compile(template, finalCompilerOptions)
      }
    }
    instance.render = (Component.render || NOOP) as InternalRenderFunction
  }

  // support for 2.x options 支持 vue2
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    setCurrentInstance(instance)
    pauseTracking()
    applyOptions(instance) // 对除了setup之外的所有选项做处理
    resetTracking()
    unsetCurrentInstance()
  }

}
复制代码

这样一来,在执行完第二步 进行组件的初始化setupComponent时,我们确保了 组件的实例有了render函数,以便在未来执行。

我们看完第二步,再看看setupRenderEffect时都做了什么。

setupRenderEffect函数

setupRenderEffect函数精简代码

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

        toggleRecurse(instance, false)
        // beforeMount hook
        if (bm) {
          invokeArrayFns(bm)
        }
        if (
          __COMPAT__ &&
          isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
        ) {
          instance.emit('hook:beforeMount')
        }
          // 渲染函数的执行过程,subTree为当前组件的子树,vnode
          const subTree = (instance.subTree = renderComponentRoot(instance))
          // patch递归遍历vnode
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          initialVNode.el = subTree.el
        }
        // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
        initialVNode = container = anchor = null as any
      } else {
        // ... 更新流程
      }
    }

    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope // track it in component's effect scope
    ))

    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
    update()
  }
复制代码

上面的代码,我们已经删除了和本教程无关的逻辑,以求让读者能更清晰的把控好本函数在挂载阶段所做的工作内容。从上面代码我们可以很清楚的看到,它主要创建了一个名为componentUpdateFn的函数。然后通过ReactiveEffectcomponentUpdateFn构建出异步函数effect,以便在以后的特定时机进行调用。

既然创建effect的作用是以后调用componentUpdateFn,那我们先分析一下这个函数的作用,也就可以清晰的知道setupRenderEffect所做的事情了。

通过上面的代码我们可以看到componentUpdateFn主要做了一下

  • 该方法会执行很多hooks
  • 构建vnode子树  const subTree = (instance.subTree = renderComponentRoot(instance))
  • 执行patch,递归的完成vnode的挂载和更新

通过上面的componentUpdateFn函数的执行,我们又回到了patch进行执行,只是这次的patch执行的是instance.subTree,所以这里会一直进行递归执行,直到所有需要子节点都执行完毕,此时我们的跟节点的patch函数才算是结束执行。

看来我们已经接近胜利了,接下来我们再继续走一遍来检查我们上面的想法是否正确。我们继续执行componmentUpdateFn函数中的patch函数,看我们断点调试的结果

Pasted image 20220208164609.png 根据type等结果,此时的section进入了processElement函数。

processElement函数


  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
复制代码

processElement函数在挂载阶段只是调用了mountElement函数,我们直接看mountElement函数。

mountElement函数

  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
      // hostCreateElement创建dom元素
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is,
        props
      )

      // mount children first, since some props may rely on child content
      // being already rendered, e.g. `<select value>`
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 递归children
        mountChildren(
          vnode.children as VNodeArrayChildren,
          el,
          null,
          parentComponent,
          parentSuspense,
          isSVG && type !== 'foreignObject',
          slotScopeIds,
          optimized
        )
      }
  }
复制代码

mountChildren函数

  const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized,
    start = 0
  ) => {
    // 循环patch
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
复制代码

我们看到mountElement函数主要做了两件事

  • hostCreateElement,该操作会创建vode对应的dom元素。
  • 如果有子节点会执行mountChildren函数。而mountChildren函数是循环调用patch函数来执行对应child过程。

关于挂载的流程我们就先分析这么多。整个过程还是相对来说比较复杂的。主要难点在于patch函数的递归调用流程。 在这里我总结了一张思维导图,放在这里仅供大家参考。

Pasted image 20220208172027.png

结语

到目前为止,我们已经完成了对vue3源码的初始化流程的分析。相信大家通过一步一步的调试一定收货匪浅。不过由于流程的复杂性建议小伙伴们多执行几遍,遇到不懂得地方多分析,最好把画出思维导图来帮助自己理清思路。

当然有问题的地方也可以留言互动,我们下期见~

猜你喜欢

转载自juejin.im/post/7062269797479907341