Skip to content

源码解读 vue3 的初始化渲染流程

准备读源码

去 github 把 vue3 的代码仓库拉下来。

在 package.json 中给 dev 脚本加上 sourcemap 参数,然后执行脚本。

js
"dev": "node scripts/dev.js --sourcemap",

然后可以在下面这个路径中新建一个文件打开,在浏览器中进行断掉调试了。

js
core/packages/vue/examples/composition/test.html

首次渲染过程

要渲染出下面这个界面,vue 要做出哪些步骤呢?

html
<script src="../../dist/vue.global.js"></script>

<div id="app">
  <h1>
    {{count}}
  </h1>
</div>
<script>
Vue.createApp({
  data() {
    return {
      count: 0    
    }
  }
}).mount("#app") // 在这里打上断点
</script>

然后进行调试

  1. 执行渲染器的 createApp 方法,返回 app 实例
  2. 解构出 app 实例的原生 mount 方法
  3. 对 app 实例的 mount 方法进行扩展,主要是
    1. 获取宿主节点

    2. 定义根组件的引用常量 component

    3. 将宿主节点的 innerHTML 赋值到根组件的 template 属性上。

    4. 处理 vue2/vue3 的兼容性问题,如果数组节点上有非 v-cloak 指令,就打印警告,它主要想说明的是:

      // vue2 是通过 outerHtml 将宿主节点整个都给替换为 vue 应用。 // 而 vue3 是通过 innerHtml 将 vue 应用作为子节点插入到宿主元素的内部。 // ===2.x compat check // 检查宿主节点的 attribute,如果包含非 v-cloak 指令,发出警告 // vue2 通过 outerHtml,将整个应用替换掉宿主节点。 // vue3 通过 innerHtml,将整个应用作为子节点插入到宿主元素中。

  4. 在首次挂载之前清空宿主节点的 innerHtml
  5. 执行扩展过后的 mount 方法,返回 proxy 常量
  6. 最后移除宿主节点上的 v-cloak 指令
  7. 返回 proxy
js
// vue 的初始化渲染流程,会去执行一个 createApp 方法,在它内部
export const createApp = ((...args) => {
  // 1. 首先执行渲染器的 createApp 方法,并返回 app 实例
  const app = ensureRenderer().createApp(...args)
  // ...
  // 2. 结构出 app 实例的元素 mount 方法
  const { mount } = app
  // 3. 对 app 实例的 mount 方法进行扩展,主要是
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 3.1 获取宿主节点
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    // _component 是从哪里来的?在执行 createApp 的时候以_componnent 属性添加在 app 实例上
    // 3.2 定义根组件的引用常量 component
    const component = app._component

    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.

      // 3.3 将宿主节点的 innerHTML 赋值到根组件的 template 属性上。
      component.template = container.innerHTML


      // 3.4 处理 vue2/vue3 的兼容性问题,如果数组节点上有非 v-cloak 指令,就打印警告,它主要想说明的是:
          // vue2 是通过 outerHtml 将宿主节点整个都给替换为 vue 应用。
          // 而 vue3 是通过 innerHtml 将 vue 应用作为子节点插入到宿主元素的内部。
          // ===2.x compat check
          // 检查宿主节点的 attribute,如果包含非 v-cloak 指令,发出警告
          // vue2 通过 outerHtml,将整个应用替换掉宿主节点。
          // vue3 通过 innerHtml,将整个应用作为子节点插入到宿主元素中。
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

    // clear content before mounting
    // 4. 在首次挂载之前清空宿主节点的innerHtml
    container.innerHTML = ''
    // 5. 执行扩展过后的mount方法,返回proxy常量
    const proxy = mount(container, false, container instanceof SVGElement)
    // 6. 最后移除宿主节点上的v-cloak指令
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    // 7 返回proxy
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
原生 mount 方法定义的位置

原生 mount 方法会去执行 render 方法。

js
    function createAppApi(render, hydrate) {
      return function createApp(rootComponent, rootProps) {
        const app = {
          use() {/** */},
          mixin() {/** */},
          component() {/** */},
          directive() {/** */},

          mount() {
            if (!isMounted) {
              // ...创建根组件的虚拟 dom
              const vnode = createVNode(
                rootComponent as ConcreteComponent,
                rootProps
              )
              // ...

              if (isHydrate && hydrate) { // 如果是服务器渲染
                hydrate(vnode as VNode<Node, Element>, rootContainer as any)
              } else { // 会走这步
                // 执行 renderer.ts 里的 render 方法
                render(vnode, rootContainer, isSVG)
              }
              isMounted = true
              // ...
            } else if (__DEV__) {
              warn(
                `App has already been mounted.\n` +
                  `If you want to remount the same app, move your app creation logic ` +
                  `into a factory function and create fresh app instances for each ` +
                  `mount - e.g. \`const createMyApp = () => createApp(App)\``
              )
            }
          },

          unmount() {/** */},
          provide() {/** */}
        }
        
        return app;
      }
    }
renderer.ts 里定义的名为 render 的内部方法
js
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 调用同样定义在 renderer.ts 文件里的名为 patch 的内部方法
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    // 这三行代码是干什么的不知道
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }
patch 方法内部会经过一系列判断,执行以下定义在 renderer.ts 里且同级的方法:
js
// 调用它
processComponent(
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized
)

// --->
    mountComponent(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    )
mountComponent 方法内部
js
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // ...
    // 初始化组件更新函数,创建数据更新副作用。
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

    // ...
  }
setupRenderEffect 方法内部
js
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 组件更新函数
    const componentUpdateFn = () => {
      if (!instance.isMounted) { // 如果没有被挂载

        if (el && hydrateNode) {
          // ...
        } else {
          // ...
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          // ...
        }
        
      } else { // 更新
        // ...
        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
        )
        // ...
      }
    }

    // 创建响应式副作用,传入参数组件更新函数
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())

    // effect.run() 方法最终其实执行的是 componentUpdateFn 方法。
    update()
  }
查看 ReactiveEffect 类
js
export class ReactiveEffect<T = any> {
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    // ...
  }

  run() {
    
    try {

      Effect(this)
      
      return this.fn()

    } finally {
      // ...
    }
  }

  stop() {

  }
}
回归正题,接下来会执行 componentUpdateFn 方法,而它的主要作用是去执行 patch 方法。

咦,这里第二次出现了 patch 方法。第一次 patch 方法是在哪里被执行的呢?

没错,是原生 mount 方法里的 render 方法里,执行了 patch 方法。

那么这一会 patch 方法会去执行哪些方法呢?

会执行以下这些方法:

js
// patch 方法经过判断,会去执行 renderer 渲染器的

processElement()
// -->
  mountElement()
    // -->
      mountChildren() // 因为内部有多个标签元素,会循环执行 mountElement 方法
        // -->
         mountElement()

Released under the MIT License.