从需求出发阅读 Vue 源码之声明式渲染(Vue 3)

tao
发布于2024-03-09 | 更新于2024-10-19
截屏2024-10-19 21.57.17.png

从需求出发阅读 Vue 源码之声明式渲染(Vue 2)这篇笔记当中解读了有关 vue@2.6.14 在声明式渲染上的特点,今天这篇笔记作为补充篇来解读 vue@3.3.13 在声明式渲染上的变化。

vue3 在架构上使用了 Monorepo(单一仓库),同时新增了很多新功能,因此在阅读源码时配合使用 VSCode 插件 CodeTour 做记录,利用 ctrl+鼠标左键 和 alt+键盘左右键 来做定位跳转,用 ChatGPT 或通义灵码做辅助理解,会让整个阅读过程更轻松一点。

Microsoft.VisualStudio.Services.Icons.png

CodeTour is a VS Code extension that allows you to record and playback guided tours of codebases, directly within the editor

创建测试环境

将 vue@3.3.13 源代码克隆下来之后在项目目录执行 pnpm install 命令安装项目依赖,然后执行 pnpm run dev 命令启动开发环境,这步操作实际上是在项目目录下面执行了:

node scripts/dev.js

script/dev.js 存在如下配置:

......
import esbuild from 'esbuild'
......
esbuild
  .context({
    entryPoints: '../packages/vue/src/index.ts',
    outfile: 'packages/vue/dist/vue.global.js',
    globalName: 'Vue',
    ......
  })
  .then(ctx => ctx.watch())
......

通过以上配置可以看出在 scripts/dev.js 中引入了 esbuild 进行打包,并指定入口地址为 packages/vue/src/index.ts,输出文件地址为 packages/vue/dist/vue.global.js,通过指定构建选项 globalName,将 ES 模块编译为一个单独的全局变量,并将全局变量命名为 Vue。

在 packages/vue/examples 文件夹里面创建测试页面 test.html,并且通过 script 标签引入输出文件 packages/vue/dist/vue.global.js,这样就可以用浏览器直接打开 test.html 进行测试,只要源码发生改变就会重新打包 vue.global.js,刷新页面就可以重新加载新的输出文件。

在 test.html 中写入以下代码用作测试,这段代码我们后文称之为测试程序,其实现的功能与从需求出发阅读 Vue 源码之声明式渲染(Vue 2)这篇笔记当中的测试程序是一样的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
    </div>
  </body>

  <script>
    const { createApp } = Vue

    const app = createApp({
      data: () => ({
        message: 'hello vue!',
      }),
    })

    app.mount('#app')
  </script>
</html>

createApp

在 vue3 中,每个 vue 应用都是通过 createApp 函数创建一个新的应用实例。

打开入口文件 packages/vue/src/index.ts,最后两行导出了 compileToFunction 函数(别名 compile)和 @vue/runtime-dom 全部的导出。

@vue/runtime-dom 实际上是 packages/runtime-dom/src 路径的别名,别名配置在根目录 tsconfig.json 中,如下所示:

{
  "compilerOptions": {
    "baseUrl": ".",
    ......
    "paths": {
      "@vue/compat": ["packages/vue-compat/src"],
      "@vue/*": ["packages/*/src"],
      "vue": ["packages/vue/src"]
    }
  },
......
}

打开 packages/runtime-dom/src/index.ts,第 65 行定义并导出了 createApp 函数,在 createApp 函数中我们主要关注 app 的生成及app 中的 mount 方法。

import {
  createRenderer,
  ......
} from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
import {
  ......
  extend,
  ......
} from '@vue/shared'

......

const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)

let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

......

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

......

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ......
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    ......
  }

  return app
}) as CreateAppFunction<Element>

app 的创建方法要调用 ensureRenderer (中文直译为确定渲染者)去确定,ensureRenderer 函数通过 createRenderer 创建,packages/runtime-dom/src/index.ts 第 2 行通过 packages/runtime-core/src/index.ts 导入了 createRenderer 方法。

打开 packages/runtime-core/src/index.ts,第 117 行通过 packages/runtime-core/src/renderer.ts 导出了 createRenderer 方法,继续打开 packages/runtime-core/src/renderer.ts,第 297 行定义了 createRenderer 方法,在方法中执行了 baseCreateRenderer 函数。

......
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
......
// overload 1: no hydration
function baseCreateRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement>

// overload 2: with hydration
function baseCreateRenderer(
  options: RendererOptions<Node, Element>,
  createHydrationFns: typeof createHydrationFunctions
): HydrationRenderer

// implementation
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ......
}
......

在 packages/runtime-core/src/index.ts 中从第 313 行开始定义 baseCreateRenderer 函数,它的定义使用到了 TS 中的函数重载,函数重载允许一个函数具有多个签名,每个签名有不同的参数类型,从而让同一个函数能处理多种不同类型的数据输入,返回不同类型的结果。

baseCreateRenderer 函数很长,粗略估计有千行以上,内部定义了很多的方法,但是函数最后第 2352 行返回了一个对象,对象属性包括了 createApp,属性值是通过第 52 行从 packages/runtime-core/src/apiCreateApp.ts 导入的 createAppAPI 生成的。

......
import { createAppAPI, ...... } from './apiCreateApp'
......
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    ......
  }

  ......

  return {
    render,
    ......
    createApp: createAppAPI(render, ......)
  }
}
......

打开 packages/runtime-core/src/apiCreateApp.ts,第 200 行定义了 createAppAPI 方法,函数内第 204 行定义并返回了 createApp 函数,在createApp 函数内创建并返回了 app,并在 app 上定义了 mount 方法,同时能看到常用的 usecomponent 等方法也在这里被定义。

......
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  ......
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    const context = createAppContext()
    ......
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,
    
      ......

      use(plugin: Plugin, ...options: any[]) { ...... },
      component(name: string, component?: Component): any { ...... },
      mount(
        rootContainer: HostElement,
        ......
      ): any {
        ......
      }
    }
  }

  ......

  return app
}

回到 packages/runtime-dom/src/index.ts 第 73 行对原始 mount 方法进行备份之后,第 74 行对 mount 方法进行了修改,执行原始 mount 方法之前,在 app._component 属性也就是执行 createApp 方法时传入的参数 rootComponent测试程序执行 createApp 函数时传入的选项)上增加了 template 属性,值为挂载 DOM 节点的 innerHTML。

......
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    ......
    return res
  }
  ......
  return container as any
}
......
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  ......
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      ......
      component.template = container.innerHTML
      ......
    }

    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    ......
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

测试程序中传入的选项至此就变为了:

{
  data: () => ({
    message: 'hello vue!',
  }),
  template: "\n      <div>{{ message }}</div>\n    "
}

mount

打开 packages/runtime-core/src/apiCreateApp.ts,第 320 行定义了原始 mount 方法。

......
import { createVNode, ...... } from './vnode'
......
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  ......
): CreateAppFunction<HostElement> {
......
    let isMounted = false
......
      mount(
        rootContainer: HostElement,
        ......
      ): any {
        if (!isMounted) {
          ......
          const vnode = createVNode(rootComponent, rootProps)
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          ......

            render(vnode, rootContainer, ......)

          ......

          isMounted = true
          app._container = rootContainer

          ......

        }
        ......
      }
......
}
......

vnode

第 334 行在 mount 函数内执行 createVNode 方法创建 vnode,第 22 行从 packages/runtime-core/src/vnode.ts 引入了 createVNode

打开 packages/runtime-core/src/vnode.ts,第 506 行定义了 createVNode 方法,最终会执行第 510 行的 _createVNode 方法,第 573 行 _createVNode 方法中确定了 shapeFlagShapeFlags.STATEFUL_COMPONENT,第 597 行执行 createBaseVNode 并返回函数执行结果。

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  ......
): VNode {

  ......

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  ......

  return createBaseVNode(
    type,
    ......
    shapeFlag,
    ......
  )
}

第 419 行定义了 createBaseVNode 函数,执行这个函数最终所返回的 vnode 结构如下(大部分属性未列出):

......
const vnode = {
  __v_isVNode: true,
  __v_skip: true,
  type,
  ......
  shapeFlag,
  ......
}
......

回到 packages/runtime-core/src/apiCreateApp.ts 第 349 行,这里将上一步生成的 vnode 和挂载 DOM 节点元素作为参数传入 render 函数执行,render 函数是在 packages/runtime-core/src/apiCreateApp.ts 中定义 createAppAPI 方法时的入参,它的定义需要回到 packages/runtime-core/src/renderer.ts 第 2318 行(在 baseCreateRenderer 那个超长函数中,注意与后文实例 instance 上的 render 属性作区分)。

......
  const render: RootRenderFunction = (vnode, container, ......) => {
    if (vnode == null) {
      ......
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, ......)
    }
    ......
    container._vnode = vnode
  }
......

patch

packages/runtime-core/src/renderer.ts 第 358 行定义了 patch 函数,在上文中确定了 vnode 的 shapeFlag 属性值为 ShapeFlags.STATEFUL_COMPONENT,因此会执行第 427 行的处理组件 processComponent 函数。

......
  const { type, ref, shapeFlag } = n2
    switch (type) {
      ......
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          ......
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            ......
          )
        }
        ......
      ......
    }
......

第 1145 行定义了 processComponent 函数,根据传入参数,会执行到第 1167 行的挂载组件 mountComponent 函数。

......
  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    ......
  ) => {
    ......
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ......
      } else {
        mountComponent(
          n2,
          container,
          ......
        )
      }
    } else {
      updateComponent(n1, n2, ......)
    }
  }
......

第 1182 行定义了 mountComponent 函数,在这个函数中我们主要关注生成 instance设置组件设置渲染副作用这三个步骤。

......
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    ......
  ) => {
    ......
    const instance: ComponentInternalInstance =
      ......
      (initialVNode.component = createComponentInstance(
        initialVNode,
        ......
      ))

    ......

    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      ......
      setupComponent(instance)
      ......
    }

    ......

    setupRenderEffect(
      instance,
      initialVNode,
      container,
      ......
    )

    ......
  }
......

第 1197 行的 createComponentInstance 函数生成 instance,第 19 行从 packages/runtime-core/src/component.ts 导入了 createComponentInstance 函数。

打开 packages/runtime-core/src/component.ts,第 485 行定义了 createComponentInstance 函数。

......
let uid = 0

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  ......
) {
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    ......
  }
  if (__DEV__) {
    instance.ctx = createDevRenderContext(instance)
  } else {
    instance.ctx = { _: instance }
  }

  ......

  return instance
}
......

其中第 574 行在开发环境下通过 createDevRenderContext 函数生成了 instance.ctx,第 17 行从 packages/runtime-core/src/componentPublicInstance.ts 中导入了 createDevRenderContext 函数。

打开 packages/runtime-core/src/componentPublicInstance.ts,第 529 行定义了 createDevRenderContext 函数,作用就是返回一个包括 _ (instance 本身)属性与 $el$data 等公用方法的对象。

......
export const publicPropertiesMap: PublicPropertiesMap =
  // Move PURE marker to new line to workaround compiler discarding it
  // due to type annotation
  /*#__PURE__*/ extend(Object.create(null), {
    $: i => i,
    $el: i => i.vnode.el,
    $data: i => i.data,
    $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
    $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
    $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
    $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
    $parent: i => getPublicInstance(i.parent),
    $root: i => getPublicInstance(i.root),
    $emit: i => i.emit,
    $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
    $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
    $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
    $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
  } as PublicPropertiesMap)
......
export function createDevRenderContext(instance: ComponentInternalInstance) {
  const target: Record<string, any> = {}

  // expose internal instance for proxy handlers
  Object.defineProperty(target, `_`, {
    configurable: true,
    enumerable: false,
    get: () => instance
  })

  // expose public properties
  Object.keys(publicPropertiesMap).forEach(key => {
    Object.defineProperty(target, key, {
      configurable: true,
      enumerable: false,
      get: () => publicPropertiesMap[key](instance),
      // intercepted by the proxy so no need for implementation,
      // but needed to prevent set errors
      set: NOOP
    })
  })

  return target as ComponentRenderContext
}
......

最终返回的 instance 结构如下(大部分属性未列出):

......
const instance: ComponentInternalInstance = {
  uid: uid++,
  vnode,
  type,
  parent,
  appContext,
  ......
  ctx: { _: instance, ...... },
  ......
}
......

生成 instance 后回到 packages/runtime-core/src/renderer.ts 第 1222 行执行 设置组件setupComponent 函数,第 21 行从 packages/runtime-core/src/component.ts 中导入了 setupComponent 函数。

打开 packages/runtime-core/src/component.ts,第 659 行定义了 setupComponent 函数,函数内执行 setupStatefulComponent 函数。

......
export function isStatefulComponent(instance: ComponentInternalInstance) {
  return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
}
......
export function setupComponent(
  instance: ComponentInternalInstance,
  ......
) {
  ......
  const isStateful = isStatefulComponent(instance)
  ......
  const setupResult = isStateful
    ? setupStatefulComponent(instance, ......)
    : undefined
  ......
  return setupResult
}

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

  ......

  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))

  ......

  finishComponentSetup(instance, ......)
}
......

设置状态组件 setupStatefulComponent 函数中为 instance 设置了 proxy 属性,值为基于 instance.ctx 的代理,第 16 行从 packages/runtime-core/src/componentPublicInstance.ts 中导入了处理器对象 PublicInstanceProxyHandlers,packages/runtime-core/src/componentPublicInstance.ts 第 297 行定义并导出了 PublicInstanceProxyHandlers,第 331 行在这个处理器对象的 get 方法中如果访问的 key 值是选项 data对应的 key 值时就直接从选项 data 上取值。

......
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance
      ......
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA
        return data[key]
      } else if (
      ......
  }
}
......

最后执行了结束组件设置 finishComponentSetup 函数,第 834 行定义了 finishComponentSetup 函数,这个函数的作用主要是生成 Component.renderinstance.render 渲染函数(两者相同),设置 instance.widthProxy 属性,并在实例上应用选项 applyOptions

......

let compile: CompileFunction | undefined
let installWithProxy: (i: ComponentInternalInstance) => void

......

export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
  installWithProxy = i => {
    if (i.render!._rc) {
      i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }
}
......
export function finishComponentSetup(
  instance: ComponentInternalInstance,
  ......
) {
  const Component = instance.type as ComponentOptions

  if (!instance.render) {
    if (...... && compile && !Component.render) {
      const template = ...... Component.template ......
      if (template) {
        ......
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const { delimiters, compilerOptions: componentCompilerOptions } =
          Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        ......
        Component.render = compile(template, finalCompilerOptions)
        ......
      }
    }
    instance.render = (Component.render || NOOP) as InternalRenderFunction

    if (installWithProxy) {
      installWithProxy(instance)
    }
  }

  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    ......
    try {
      applyOptions(instance)
    } finally {
      ......
    }
  }
}
......

packages/runtime-core/src/component.ts 第 822 行定义并导出了 registerRuntimeCompiler 函数,packages/runtime-core/src/index.ts 第 134 行从 packages/runtime-core/src/component.ts 中导出了 registerRuntimeCompiler 函数。

......
export { registerRuntimeCompiler, ...... } from './component'
......

packages/runtime-dom/src/index.ts 第 254 行导出了所有 packages/runtime-core/src/index.ts 中的导出。

......
export * from '@vue/runtime-core'
......

在 packages/vue/src/index.ts 第 5 行从 packages/runtime-dom/src/index.ts 中导入了 registerRuntimeCompiler 函数并在第 109 行使用,因此编译函数 compile 是在 packages/vue/src/index.ts 第 109 行被确定的。

......
import { compile, ...... } from '@vue/compiler-dom'
import { registerRuntimeCompiler, ...... } from '@vue/runtime-dom'
......

function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  const { code } = compile(template, opts)

  ......

  const render = (
    __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
  ) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true

  return ...... render ......
}

registerRuntimeCompiler(compileToFunction)
......

packages/vue/src/index.ts 第 34 行定义了 compileToFunction 函数,第 79 行函数内执行 compile 函数(注意与上文中 finishComponentSetup 函数中的 compile 函数作区分)生成编译代码 codecompile 函数在 packages/compiler-dom/src/index.ts 第 40 行定义。

......
export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  return baseCompile(
    template,
    extend({}, ......, options, { ...... })
  )
}
......

第 44 行函数内执行了 baseCompile 函数,打开 packages/compiler-core/src/compile.ts,第 61 行定义了 baseCompile 函数,第 85 行函数内执行 baseParse 函数生成抽象语法树 ast,第 112 行的 generate 函数使用 ast 生成 code。

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  ......
  const ast = isString(template) ? baseParse(template, options) : template
  ......
  return generate(
    ast,
    ......
  )
}

抽象语法树 ast 的生成和 generate 函数生成 code 还是比较繁琐的一件事情,在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)这篇笔记当中我们曾花费了很长的篇幅去了解,虽然 vue3 较 vue2 可以说实现方式完全不一样,但目的是相同的,这里就不再赘述。

回到 packages/vue/src/index.ts 第 79 行,拿到 code 后就可以在第 99 行生成 render 函数返回,并赋值给 packages/runtime-core/src/components.ts 第 886 行的 Component.renderinstance.render,这样测试程序中传入的选项现在就变成了:

{
  data: () => ({
    message: 'hello vue!',
  }),
  template: "\n      <div>{{ message }}</div>\n    ",
  render: new Function(
    const _Vue = Vue

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
        return (_openBlock(), _createElementBlock("div", null, _toDisplayString(message), 1 /* TEXT */))
      }
    }
  )()
}

生成 render 函数后接着在 finishComponentSetup 函数中执行 installWithProxy 函数在 instance 上绑定 withProxy 属性,它是 instance.ctx 属性的代理,并以 RuntimeCompiledPublicInstanceProxyHandlers (上文 PublicInstanceProxyHandlers 的扩展)作为处理对象。

......
i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
......

finishComponentSetup 函数中继续执行 applyOptions 函数,其在 packages/runtime-core/src/component.ts 第 42 行从 packages/runtime-core/src/componentOptions.ts 导入,打开 packages/runtime-core/src/componentOptions.ts,第 610 行定义并导出了 applyOptions 函数,在 applyOptions 函数中会执行我们在测试程序传入的选项 data 函数并将返回值 { message: 'hello vue!' } 赋值给 instance.data,开发环境中还会将返回值属性暴露到 instance.ctx 上。

......
export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance) // instance.type
  const publicThis = instance.proxy! as any
  const ctx = instance.ctx

  ......

  const {
    // state
    data: dataOptions,
    ......
  } = options

  ......

  if (dataOptions) {
    ......
    const data = dataOptions.call(publicThis, publicThis)
    ......
    if (!isObject(data)) {
      __DEV__ && warn(`data() should return an object.`)
    } else {
      instance.data = reactive(data)  // 响应式实现
      if (__DEV__) {
        for (const key in data) {
          ......
          // expose data on ctx during dev
          // export const isReservedPrefix = (key: string) => key === '_' || key === '$'
          if (!isReservedPrefix(key[0])) {
            Object.defineProperty(ctx, key, {
              configurable: true,
              enumerable: true,
              get: () => data[key],
              set: NOOP
            })
          }
        }
      }
    }
  }
  ......
}
......

至此 packages/runtime-core/src/renderer.ts 第 1222 行 setupComponent 函数执行完成,继续执行第 1242 行的设置渲染副作用 setupRenderEffect 函数。

setupRenderEffect 函数定义在 packages/runtime-core/src/renderer.ts 第 1292 行,函数内定义了一个名叫 componentUpdateFn 的超长函数,然后 new ReactiveEffect 类新建了一个响应副作用实例。

......
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    ......
  ) => {
    const componentUpdateFn = () => {
      ......
    }

    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      ......
    ))

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

    ......

    update()
  }
......

ReactiveEffect 定义在 packages/reactivity/src/effect.ts 第 53 行,在 TypeScript 中,如果在构造函数参数前面使用了访问修饰符(如 public、protected、private),TypeScript 会自动将其声明为类的成员变量,并在构造函数中进行初始化,这种语法称为参数属性。ReactiveEffect 类在构造函数中使用了这种语法,并把传入的 componentUpdateFn 函数声明为类的 fn 变量,并在类的 run 方法中执行了这个函数。

......
export class ReactiveEffect<T = any> {
  ......
  constructor(
    public fn: () => T,
    ......
  ) {
    ......
  }

  run() {
    ......
    return this.fn()
    ......
  }
......

回到 packages/runtime-core/src/renderer.ts, 第 1551 行定义了一个执行 effect.runupdate 函数,并在第 1567 行执行了 update 函数,实际执行的其实就是 componentUpdateFn 函数,在 componentUpdateFn 中会执行到第 1368 行通过 renderComponentRoot 函数生成 subTree,然后用作参数继续执行上文中的 patch 函数。

......
    const componentUpdateFn = () => {
          ......
          const subTree = (instance.subTree = renderComponentRoot(instance))
          ......
          patch(
            null,
            subTree,
            container,
            ......
          )
          ......
    }
......

打开 packages/runtime-core/src/componentRenderUtils.ts,第 43 行定义了 renderComponentRoot 函数,第 91 行通过执行上文的渲染函数 instance.render 生成 result 作为 root 并返回。

export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    ......
    render,
    ......
  } = instance

  let result
  ......
      const proxyToUse = withProxy || proxy
      // 'this' isn't available in production builds with `<script setup>`,
      // so warn if it's used in dev.
      const thisProxy = ...... proxyToUse
      result = ......
        render!.call(
          thisProxy,
          proxyToUse!,
          ......
        )
      ......
  ......
  let root = result
  ......
  return root
}

上文生成的 render 函数为如下形式:

  render: new Function(
    const _Vue = Vue

    return function render(_ctx, _cache) {
      with (_ctx) {
        const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
        return (_openBlock(), _createElementBlock("div", null, _toDisplayString(message), 1 /* TEXT */))
      }
    }
  )()

执行 render 函数首先执行 openBlock 函数,这个函数定义在 packages/runtime-core/src/vnode.ts 第 251 行。

......
export const blockStack: (VNode[] | null)[] = []
export let currentBlock: VNode[] | null = null

export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}
......

然后执行 toDisplayString 函数,定义在 packages/shared/src/toDisplayString.ts 第 17 行。

......
export const toDisplayString = (val: unknown): string => {
  return isString(val)
    ? val
    : val == null
      ? ''
      : isArray(val) ||
          (isObject(val) &&
            // export const objectToString = Object.prototype.toString
            (val.toString === objectToString || !isFunction(val.toString)))
        ? JSON.stringify(val, ......, 2)
        : String(val)
}
......

最后执行 createElementBlock 函数,定义在 packages/runtime-core/src/vnode.ts 第 303 行,其中会执行上文提到过的 createBaseVnode 函数。

......
export function closeBlock() {
  blockStack.pop()
  currentBlock = blockStack[blockStack.length - 1] || null
}

// Whether we should be tracking dynamic child nodes inside a block.
// Only tracks when this value is > 0
// We are not using a simple boolean because this value may need to be
// incremented/decremented by nested usage of v-once (see below)
export let isBlockTreeEnabled = 1
......
export function createElementBlock(
  type: string | typeof Fragment,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  ......
) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */
    )
  )
}
......
function setupBlock(vnode: VNode) {
  // save current block children on the block vnode
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  // close block
  closeBlock()
  // a block is always going to be patched, so track it as a child of its
  // parent block
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}
......

这样来看,packages/runtime-core/src/renderer.ts 第 1368 行通过 renderComponentRoot 函数生成的 subTree 实际就是一个 vnode,并且在 packages/runtime-core/src/vnode.ts 执行 createBaseVNode 函数第 467 行确认了 shapeFlag 是 vnode.shapeFlagvnode.shapeFlagShapeFlags.ELEMENT)与 ShapeFlags.TEXT_CHILDREN 的位或操作结果。subtree 的结构如下(大部分属性未列出):

const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type: 'div',
    children: "hello vue!",
    shapeFlag: 9,
    ......
}

回到 packages/runtime-core/src/renderer.ts,在 1368 行生成 subtree 后继续执行1375 行的 patch 方法,这次再回到 patch 函数会走到第 415 行处理元素 processElement 函数,随后在第 590 行执行 mountElement 方法,在 mountElement 函数中会通过 hostCreateElement 方法生成 vnode.el 的值,hostCreateElement 函数是上文在确认渲染方法(ensureRenderer)的时候传入的渲染参数中的一个选项 createElement

......
  const {
    ......
    createElement: hostCreateElement,
    ......
  } = options
......
  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    ......
  ) => {
    let el: RendererElement
    ......

    el = vnode.el = hostCreateElement(
      vnode.type as string,
      ......
    )
  }
......

打开 packages/runtime-dom/src/nodeOps.ts 第 21 行定义了 createElement 函数如下所示通过 document.createElement 函数生成对应 tag 的 DOM 元素:

......
const doc = (typeof document !== 'undefined' ? document : null) as Document
......
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  ......
  createElement: (tag, ......): Element => {
    const el = ...... doc.createElement(tag, ......)
    ......
    return el
  },
  ......
}
......

接下来处理文本子节点,与上述的 hostCreateElement 一样,第 637 行的 hostSetElementText 函数就是传入选项 setElementText 函数。

......
  const {
    ......
    setElementText: hostSetElementText,
    ......
  } = options
......
  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    ......
  ) => {
    let el: RendererElement
    ......
    const { ......, shapeFlag, ...... } = vnode
    ......

    el = vnode.el = hostCreateElement(
      vnode.type as string,
      ......
    )

    ......
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, vnode.children as string)
    }
  }
......

打开 packages/runtime-dom/src/nodeOps.ts 第 41 行定义了 setElementText 函数如下所示通过设置节点的 textContent 属性挂载文本:

......
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  ......
  setElementText: (el, text) => {
    el.textContent = text
  },
  ......
}
......

最后第 709 行用 hostInsert 关联 packages/runtime-dom/src/nodeOps.ts 的 insert 函数将刚创建的 DOM 元素插入到父节点上。

......
  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    ......
  ) => {
    let el: RendererElement
    ......

    hostInsert(el, container, ......)
    ......
  }
......

至此渲染完成。