Vue3源码解读(2)-初始化过程

472 阅读8分钟

通过本文我们将深入理解 Vue3 的初始化过程,即:

const app = createApp({}).mount('#demo')

这步发生了什么。

我们再看下我们在上篇文章中提到的 demo:

packages/vue/examples/composition/commits.html

<script src="../../dist/vue.global.js"></script>
<div id="demo">
  <h1>Latest Vue.js Commits</h1>
  <template v-for="branch in branches">
    <input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch">
    <label :for="branch">{{ branch }}</label>
  </template>
  <p>vuejs/vue@{{ currentBranch }}</p>
  <ul>
    <li v-for="{ html_url, sha, author, commit } in commits">
      <a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
      - <span class="message">{{ truncate(commit.message) }}</span><br>
      by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
      at <span class="date">{{ formatDate(commit.author.date) }}</span>
    </li>
  </ul>
</div>

<script>
  const { createApp, ref, watchEffect } = Vue
  const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`
  // 截断、删除
  const truncate = v => {
    const newline = v.indexOf('\n')
    return newline > 0 ? v.slice(0, newline) : v
  }
  const formatDate = v => v.replace(/T|Z/g, ' ')
  // createApp 调用的是 runtime-dom/src/index.ts#createApp ,
  // 其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate
        }
      }
    });
    debugger
  // app.mount 调用的是 runtime-dom/src/index.ts#mount ,
  // 其封装了 packages/runtime-core/src/apiCreateApp.ts#mount
  app.mount('#demo')
</script>

<style>
		/* 
省略 css 代码
    */
</style>

1. createApp

首先看下 createApp 做了什么。

createApp 调用的是 runtime-dom/src/index.ts#createApp ,其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp。我们依次看下这两个方法。

// packages/runtime-dom/src/index.ts
const rendererOptions = {
  patchProp,  // 处理 props 属性 
  ...nodeOps // 处理 DOM 节点操作
}

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer | HydrationRenderer

let enabledHydration = false

// ensureRenderer 封装了 packages/runtime-core/src/renderer.ts#createRenderer
// renderer.ts#createRenderer 又封装了 baseCreateRenderer
// baseCreateRenderer 我们后面会讲
function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}

// 封装 createApp
export const createApp = ((...args) => {
  // app 就是上面 createApp 返回的应用对象
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 封装 mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // normalizeContainer 获取选择器对应的 dom 元素
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // app._component 就是传给 createApp 的 options
    const component = app._component
    // 传给 createApp 的 options 不包含 render 和 template,则获取 container.innerHTML
    // 用于后续编译成 render
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    // 首先清空我们挂载容器的 innerHTML
    container.innerHTML = ''
    // component.render 不存在的话, mount 阶段会编译 component.template 为 render
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

从上面的分析可以看出,createApp 其实是封装了 baseCreateRenderer 方法返回的 createApp 方法,我们接下来看下这个 baseCreateRenderer 方法。

2. baseCreateRenderer

我们看下上面提到的 baseCreateRenderer,baseCreateRenderer 这个方法足足有差不多 2000 行,baseCreateRenderer 方法里面还定义了很多方法,为了便于阅读进行了删减。

在初始化过程中,我们只需要关注 baseCreateRenderer 方法返回了什么内容即可,方法内定义的 patch 、mountElement 等方法可以以后再回来看解释。你可以直接切换到这块代码的最后查看 baseCreateRenderer 的返回值

// packages/runtime-core/src/renderer.ts
/**
  baseCreateRenderer 中定义了 patch、processElement 等一些列方法,
  用于完成节点的 diff、mount(挂载)、unmount(卸载)等逻辑,我们稍后再看这些方法的具体实现。
  
  从 baseCreateRenderer 的返回值可以看到,ensureRenderer() 的返回值就是 createAppAPI(render, hydrate) 的结果
  我们接下来再看看 createAppAPI 的实现 
 */
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // 代码太多,省略一部分...

  // Note: functions inside this closure should use `const xxx = () => {}`
  // style in order to prevent being inlined by minifiers.
  // Patch 方法中进行节点的挂载、更新、卸载
  const patch: PatchFn = (
    n1, // 旧的 VNode
    n2, // 新的 VNode
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = false
  ) => {
    // 如果有旧VNode,且新旧节点不一样,umount销毁旧节点
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    // 先通过节点类型 type 来判断选择处理方法
    // 首次挂载时 n1 = null ,n2 是根 vnode ,代表 rootComponent 
    // 所以会走 processComponent 逻辑
    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) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 首次挂载会走 processComponent 逻辑,将 n2 渲染到 #demo 节点下
          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__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }
    // ...
  }

  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
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      // 旧的节点 n1 不存在时,就走 mount(挂载) 逻辑
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      // 新旧节点 n2 n1 都存在,则执行更新逻辑
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

  // ...
	// 处理组件挂载与更新的逻辑
  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) {
      // 如果是 keep-alive 组件
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 首次挂载 n1 = null, n2 = 根 vnode
        // 执行 mountComponent
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      // 如果n1 n2 都有则执行更新
      updateComponent(n1, n2, optimized)
    }
  }

  /**
  可见,在mountComponent中主要做了三件事

  createComponentInstance,初始化组件实例,组件实例包括appContext、parent、root、props、attrs、slots、refs等属性
  setupComponent,完善instance,

  调用initProps、initSlots,初始化instance相关属性,
  外还会通过setupStatefulComponent调用传入的setup方法,获取返回值setupResult,根据其数据类型
  finishComponentSetup,

  检测instance.render是否存在,不存在则调用compile(Component.template)编译渲染函数
  在__FEATURE_OPTIONS__配置下调用applyOptions兼容Vue2.x,合并配置项到vue组件实例,初始化watch、computed、methods等配置项,调用相关生命周期钩子等

  setupRenderEffect,主要是实现instance.update方法,该方法等价于effect(function componentEffect(){...}),程序如何渲染和更新视图就在这里,这也是接下来阅读的重点
 */
  const mountComponent: MountComponentFn = (
    initialVNode, // 初始VNode 也就是App组件生成的VNode
    container, // #app Dom容器
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 创建组件实例
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    // resolve props and slots for setup context

    // 设置 instance 实例,初始化 props,slots 还有Vue3新增的composition API
    // 如果 instance.render 属性不存在,则编译 template 获得 render 并赋给 instance.render
    setupComponent(instance)
    
    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      // Give it a placeholder if this is not hydration
      // TODO handle self-defined fallback
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

  }

  /**
  instance 当前 vm 实例
  initialVNode 可以是组件 VNode 或者普通 VNode
  container 挂载的容器,例如 #demo 对应的节点
  anchor, parentSuspense, isSVG 普通情况下都为 null
   */
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering
    // 创建响应式的副作用render函数
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance

        // beforeMount hook
        // bm 生命周期 及 hook 执行
        if (bm) {
          invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if ((vnodeHook = props && props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parent, initialVNode)
        }

        // render
        // ...
        const subTree = (instance.subTree = renderComponentRoot(instance))
        // ...

        if (el && hydrateNode) {
	        // ...
          // vnode has adopted host node - perform hydration instead of mount.
          hydrateNode(
            initialVNode.el as Node,
            subTree,
            instance,
            parentSuspense,
            null
          )
	        // ...
        } else {
	        // ...
          // 把 subTree 挂载到Dom容器中
          // subTree 是什么? 例如最开始的例子 App 组件为 initialVNode,subTree 就是 App组件模版里的结构生成的VNode,
          // children 属性为 HelloWorld 组件VNode, p 标签VNode。
          // 而App组件 initialVNode 的 chidren 里面,根据 HelloWorld 标签生成的 VNode, 
          // 对于 HelloWorld 组件内部DOM结构来说就是 initialVNode,而其内部DOM结构生成的VNode就是 subTree。
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
	        // ...
          initialVNode.el = subTree.el
        }
        // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if ((vnodeHook = props && props.onVnodeMounted)) {
          const scopedInitialVNode = initialVNode
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
          }, parentSuspense)
        }
        // activated hook for keep-alive roots.
        // #1742 activated hook must be accessed after first render
        // since the hook may be injected by a child keep-alive
        const { a } = instance
        if (
          a &&
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
          queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true

        // #2458: deference mount-only object parameters to prevent memleaks
        initialVNode = container = anchor = null as any
      } else {
        // 更新组件的逻辑
        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode)
        let { next, bu, u, parent, vnode } = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined

        if (next) {
          next.el = vnode.el
          updateComponentPreRender(instance, next, optimized)
        } else {
          next = vnode
        }

        // beforeUpdate hook
        if (bu) {
          invokeArrayFns(bu)
        }
        // onVnodeBeforeUpdate
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
          invokeVNodeHook(vnodeHook, parent, next, vnode)
        }

        // render
        const nextTree = renderComponentRoot(instance)
        const prevTree = instance.subTree
        instance.subTree = nextTree

        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
        )

        next.el = nextTree.el
        if (originNext === null) {
          // self-triggered update. In case of HOC, update parent component
          // vnode el. HOC is indicated by parent instance's subTree pointing
          // to child component's vnode
          updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {
          queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, next!, vnode)
          }, parentSuspense)
        }

      }
      /**
  createDevEffectOptions(instance) 用于后续的派发更新,它会返回一个对象:
  {
      scheduler: queueJob(job) {
                      if (!queue.includes(job)) {
                          queue.push(job);
                          queueFlush();
                      }
                  },
      onTrack: instance.rtc ? e => invokeHooks(instance.rtc, e) : void 0,
      onTrigger: instance.rtg ? e => invokeHooks(instance.rtg, e) : void 0
  }
       */
    }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  }

  const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean
  ) => {
    nextVNode.component = instance
    const prevProps = instance.vnode.props
    instance.vnode = nextVNode
    instance.next = null
    // 调用updateProps、updateSlots等方法更新相关数据
    updateProps(instance, nextVNode.props, prevProps, optimized)
    updateSlots(instance, nextVNode.children, optimized)

    pauseTracking()
    // props update may have triggered pre-flush watchers.
    // flush them before the render update.
    flushPreFlushCbs(undefined, instance.update)
    resetTracking()
  }

  const removeFragment = (cur: RendererNode, end: RendererNode) => {
    // For fragments, directly remove all contained DOM nodes.
    // (fragment child nodes cannot have transition)
    let next
    while (cur !== end) {
      next = hostNextSibling(cur)!
      hostRemove(cur)
      cur = next
    }
    hostRemove(end)
  }

  // container 就是我们要挂载的那个元素,即 rootContainer
  // vnode 是使用根组件创建的根 vnode
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    // unmount 逻辑
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
      // patch 逻辑,首次挂载会走这里
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

  // ...

  return {
    render,
    hydrate, // 用于SSR渲染
    createApp: createAppAPI(render, hydrate)
  }
}

可以看到,createApp 方法是调用 createAppAPI(render, hydrate) 返回的,render 是在 baseCreateRenderer 中定义的,hydrate 用于SSR渲染,这里先不管。

我们来看一下 createAppAPI 方法。

3.createAppAPI

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    /**
    createAppAPI 工厂函数接收 render ,返回真正的 createApp 方法.
    其中 render 就是上面提到的 baseCreateRenderer 中定义的 render 方法

    app 就是实际返回的 app 对象,app 对象上定义的 use、mixin、component 
    等方法大多会返回 app 对象,因此这些方法支持链式调用,我们会在后面的文章中详细介绍这些方法,
    现在我们先继续看 Vue3 的初始化过程
    **/
    const app: App = (context.app = {
      
      // 其他 app 实例方法...

      // rootContainer 就是我们要挂载的那个元素,比如 id 为 demo 的节点
      // app.mount('#demo') 调用的就是这里的 mount 方法,执行挂载逻辑
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        // mount 方法的具体实现,下面有讲
      },

    })

    return app
  }
}

从上面的代码中我们可以看出工厂函数 createAppAPI 返回了 createApp 方法, createApp 方法最后返回了 app 对象,然后我们调用 app.mount 方法挂载节点。

Vue3 源码解读