从需求出发阅读 Vue 源码之声明式渲染(Vue 3)
在从需求出发阅读 Vue 源码之声明式渲染(Vue 2)这篇笔记当中解读了有关 vue@2.6.14 在声明式渲染上的特点,今天这篇笔记作为补充篇来解读 vue@3.3.13 在声明式渲染上的变化。
vue3 在架构上使用了 Monorepo(单一仓库),同时新增了很多新功能,因此在阅读源码时配合使用 VSCode 插件 CodeTour 做记录,利用 ctrl+鼠标左键 和 alt+键盘左右键 来做定位跳转,用 ChatGPT 或通义灵码做辅助理解,会让整个阅读过程更轻松一点。
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
方法,同时能看到常用的 use
、component
等方法也在这里被定义。
......
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
方法中确定了 shapeFlag
为 ShapeFlags.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.render
和 instance.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
函数作区分)生成编译代码 code,compile
函数在 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.render
和 instance.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.run
的 update
函数,并在第 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.shapeFlag
(vnode.shapeFlag
是 ShapeFlags.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, ......)
......
}
......
至此渲染完成。