前言
vue 有个社区贡献者 ubugeeei
(推特/X ID: @ubugeeei)是 vue 官方 Discord 的长期活跃者,经常解答新特性、源码实现相关问题,他对 vue 的源码非常熟悉,也是 Vue Vapor(Vue 无虚拟 DOM 草案)的早期研究者之一,他发布的 "Reading vuejs/core-vapor
" 阅读指南站点旨在帮助读者完整梳理 Vapor 模式的编译输出与实现细节,由于 ubugeeei
是一个日本开发者,这个站点只有日语和英语,并且中文互联网上关于这本书的介绍很少,而且我觉得内容很好,因此这里我将这个网站内容翻译成 中文 供大家阅读,由于掘金单篇文章字符限制问题,这里拆分成三部曲进行发布,本篇为下卷

关于作者
ubugeeei

-
Vue.js
成员和Vue.js
日本用户组的核心成员。 -
从 2023 年 11 月起参与
Vapor
模式的发展。 -
2023 年 12 月成为
vuejs/core-vapor
的外部合作者。 -
2024 年 4 月加入
Vue.js
组织,成为Vapor
团队的一员。
作者博客:ublog.dev/
Vue Vapor
的实现最初始于一个名为vuejs/core-vapor
的仓库,但在 2024 年 10 月更名为vuejs/vue-vapor
。在本文档中,链接已更改为vuejs/vue-vapor
,但由于本文档的项目名称和页面,文本已统一为vuejs/core-vapor
。注意,在你阅读时vuejs/core-vapor
=vuejs/vue-vapor
。它们在时间线上是不同的名称,但指的是完全相同的事物。
v-on 指令
现在让我们看一下 v-on
指令。
v-on
有两种类型:一种用于原生元素,一种用于组件。由于我还不知道如何处理组件,所以我将在这里解释用于原生元素的那种。
(顺便说一下,当涉及到组件时,它主要是 props,所以没有太多需要解释的。)
让我们考虑一个像下面这样的组件。
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button type="button" @click="increment">{{ count }}</button>
</template>
这是一个常见的计数器组件。
编译结果
编译结果如下所示。
js
const _sfc_main = {
vapor: true,
name: "App",
setup(props, { expose: expose }) {
expose();
const count = ref(0);
function increment() {
count.value++;
}
const __returned__ = { count, increment, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
delegate as _delegate,
renderEffect as _renderEffect,
setText as _setText,
delegateEvents as _delegateEvents,
template as _template,
} from "vue/vapor";
const t0 = _template('<button type="button"></button>');
_delegateEvents("click");
function _sfc_render(_ctx) {
const n0 = t0();
_delegate(n0, "click", () => _ctx.increment);
_renderEffect(() => _setText(n0, _ctx.count));
return n0;
}
像往常一样,脚本部分不是很重要,所以让我们专注于以下部分。
js
import {
delegate as _delegate,
renderEffect as _renderEffect,
setText as _setText,
delegateEvents as _delegateEvents,
template as _template,
} from "vue/vapor";
const t0 = _template('<button type="button"></button>');
_delegateEvents("click");
function _sfc_render(_ctx) {
const n0 = t0();
_delegate(n0, "click", () => _ctx.increment);
_renderEffect(() => _setText(n0, _ctx.count));
return n0;
}
理解概述
模板的生成和 renderEffect
~ setText
像往常一样。
这次,主要部分是
js
_delegateEvents("click");
和
js
_delegate(n0, "click", () => _ctx.increment);
正如预期的那样,后者可能是向 n0
添加 click
事件。
然而,我不理解"delegate"是什么意思,或者前面的 _delegateEvents
在做什么。
现在,让我们暂时将这个作为一个谜,看看编译器的实现。
当我们继续阅读运行时时,我们将理解这个谜。
阅读编译器
IR
像往常一样,让我们看一下 IR
。
有一个可疑的东西叫做 SET_EVENT
,但我没有看到其他东西。

让我们看一看。
ts
export interface SetEventIRNode extends BaseIRNode {
type: IRNodeTypes.SET_EVENT
element: number
key: SimpleExpressionNode
value?: SimpleExpressionNode
modifiers: {
// modifiers for addEventListener() options, e.g. .passive & .capture
options: string[]
// modifiers that needs runtime guards, withKeys
keys: string[]
// modifiers that needs runtime guards, withModifiers
nonKeys: string[]
}
keyOverride?: KeyOverride
delegate: boolean
/** Whether it's in effect */
effect: boolean
}
似乎这个 Node 有一个 delegate
标志。

然后,让我们寻找生成这个 Node 的转换器。
找到了。它是 packages/compiler-vapor/src/transforms/vOn.ts。
ts
const operation: SetEventIRNode = {
type: IRNodeTypes.SET_EVENT,
element: context.reference(),
key: arg,
value: exp,
modifiers: {
keys: keyModifiers,
nonKeys: nonKeyModifiers,
options: eventOptionModifiers,
},
keyOverride,
delegate,
effect: !arg.isStatic,
}
DirectiveTransform
由于这是 DirectiveTransform 第一次出现,让我们看看它是如何被调用的。
DirectiveTransform 是从 transformElement
调用的。
具体来说,它是在处理元素属性期间被调用的。
ts
export const transformElement: NodeTransform = (node, context) => {
↓
ts
const propsResult = buildProps(
node,
context as TransformContext<ElementNode>,
isComponent,
)
↓
ts
export function buildProps(
node: ElementNode,
context: TransformContext<ElementNode>,
isComponent: boolean,
): PropsResult {
↓
ts
const result = transformProp(prop, node, context)
↓
ts
function transformProp(
prop: VaporDirectiveNode | AttributeNode,
node: ElementNode,
context: TransformContext<ElementNode>,
): DirectiveTransformResult | void {
↓
ts
const directiveTransform = context.options.directiveTransforms[name]
if (directiveTransform) {
return directiveTransform(prop, node, context)
}
在这种情况下,它从像 on
这样的名称中获取 v-on 转换器,并调用 transformVOn
。
然后,在 transformVOn
中,在最后调用 context.registerEffect
,注册效果。
ts
context.registerEffect([arg], operation)
transformVOn
现在,让我们看一下 transformVOn
。
dir
是指令的 AST。 这是在 runtime-core
中实现的,并在解析阶段创建的。
从这里,我们提取 arg
、expr
、modifiers
等。
ts
let { arg, exp, loc, modifiers } = dir
简单解释一下,在 v-on:click.stop="handler"
中,click
对应于 arg
,stop
对应于 modifiers
,handler
对应于 expr
。
首先,我们按类型解析和组织 modifiers
。
ts
const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
resolveModifiers(
arg.isStatic ? `on${arg.content}` : arg,
modifiers,
null,
loc,
)
resolveModifiers
是在 compiler-dom
中实现的函数,用于对修饰符进行分类。
ts
export const resolveModifiers = (
key: ExpressionNode | string,
modifiers: string[],
context: TransformContext | null,
loc: SourceLocation,
): {
keyModifiers: string[]
nonKeyModifiers: string[]
eventOptionModifiers: string[]
} => {
接下来,我们确定是否启用 delegate
。(现在,让我们暂时不考虑 delegate
是什么。)
ts
const delegate =
arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)
当满足以下所有条件时,它被启用:
arg
是静态的
这是当它不是像v-on[eventName]="handler"
这样的东西时。modifiers
为空。- 它是一个委托目标
这是对它是否是这里定义的事件的判断。
ts
const delegatedEvents = /*#__PURE__*/ makeMap(
'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' +
'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' +
'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' +
'touchstart',
)
之后,基于到目前为止获得的信息,用 registerEffect
注册效果完成了这个过程。
ts
const operation: SetEventIRNode = {
type: IRNodeTypes.SET_EVENT,
element: context.reference(),
key: arg,
value: exp,
modifiers: {
keys: keyModifiers,
nonKeys: nonKeyModifiers,
options: eventOptionModifiers,
},
keyOverride,
delegate,
effect: !arg.isStatic,
}
context.registerEffect([arg], operation)
阅读代码生成
让我们主要关注 delegate
标志如何影响事物,并略过其余部分。
ts
export function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
ts
case IRNodeTypes.SET_EVENT:
return genSetEvent(oper, context)
ts
export function genSetEvent(
oper: SetEventIRNode,
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
const { element, key, keyOverride, value, modifiers, delegate, effect } = oper
const name = genName()
const handler = genEventHandler(context, value)
const eventOptions = genEventOptions()
if (delegate) {
// key is static
context.delegates.add(key.content)
}
return [
NEWLINE,
...genCall(
vaporHelper(delegate ? 'delegate' : 'on'),
`n${element}`,
name,
handler,
eventOptions,
),
]
这个流程现在很熟悉,应该没有困难的部分。
我特别想强调的是这里。
ts
vaporHelper(delegate ? 'delegate' : 'on'),
当 IR
中的 delegate
被启用时,它生成一个 delegate
辅助函数;否则,它生成一个 on
辅助函数。
换句话说,当接下来阅读运行时时,比较这两者应该有助于你理解 delegate
的作用。
你还可以看到,就在这之前,事件被注册在 context.delegates
中。
你也可以理解,这可能是提升的 _delegateEvents("click");
部分。
ts
if (delegate) {
// key is static
context.delegates.add(key.content)
}
阅读运行时
现在,有三个我想阅读的函数。
它们是 delegateEvents
、delegate
和 on
。
首先,让我们按照执行顺序看一下 delegateEvents
。
delegateEvents
实现如下。
ts
/**
* Event delegation borrowed from solid
*/
const delegatedEvents = Object.create(null)
export const delegateEvents = (...names: string[]): void => {
for (const name of names) {
if (!delegatedEvents[name]) {
delegatedEvents[name] = true
// eslint-disable-next-line no-restricted-globals
document.addEventListener(name, delegatedEventHandler)
}
}
}
从注释中可以看出,这个概念似乎是从 Solid 借鉴的。
查看 Solid 的文档,有关于 delegate 的解释。
docs.solidjs.com/concepts/co...
Solid 提供了两种向浏览器添加事件监听器的方式:
on:__
:向元素添加事件监听器。这也被称为原生事件。
on__
:向文档添加事件监听器并将其分派到元素。这可以称为委托事件。委托事件流经组件树,通过在常用事件上表现更好来节省一些资源。然而,原生事件流经 DOM 树,并提供对事件行为的更多控制。
delegateEvents
在文档上监听传递的事件的 delegatedEventHandler
。
让我们看一下 delegatedEventHandler
。
ts
const delegatedEventHandler = (e: Event) => {
let node = ((e.composedPath && e.composedPath()[0]) || e.target) as any
if (e.target !== node) {
Object.defineProperty(e, 'target', {
configurable: true,
value: node,
})
}
Object.defineProperty(e, 'currentTarget', {
configurable: true,
get() {
// eslint-disable-next-line no-restricted-globals
return node || document
},
})
while (node !== null) {
const handlers = getMetadata(node)[MetadataKind.event][e.type]
if (handlers) {
for (const handler of handlers) {
if (handler.delegate && !node.disabled) {
handler(e)
if (e.cancelBubble) return
}
}
}
node =
node.host && node.host !== node && node.host instanceof Node
? node.host
: node.parentNode
}
}
e.composedPath
是一个方法,它以数组形式返回事件的路径(EventTarget)。
developer.mozilla.org/en-US/docs/...
js
// 示例
e.composedPath(); // [button, div, body, html, document]
首先,使用一个名为 getMetadata
的函数,它从 node
中检索元数据,并从那里的事件信息中获取处理程序。\
ts
const handlers = getMetadata(node)[MetadataKind.event][e.type]
然后,它执行所有这些处理程序。
ts
if (handlers) {
for (const handler of handlers) {
if (handler.delegate && !node.disabled) {
handler(e)
之后,它通过遍历主机和父级来传播这个流程。

让我们也在这个流程中阅读 delegate
。
delegate
ts
export function delegate(
el: HTMLElement,
event: string,
handlerGetter: () => undefined | ((...args: any[]) => any),
options: ModifierOptions = {},
): void {
const handler: DelegatedHandler = eventHandler(handlerGetter, options)
handler.delegate = true
recordEventMetadata(el, event, handler)
}
delegate
创建一个处理程序,并在元数据中注册它,设置委托标志。
recordEventMetadata
在另一个文件 packages/runtime-vapor/src/componentMetadata.ts 中实现。
ts
export function recordPropMetadata(el: Node, key: string, value: any): any {
const metadata = getMetadata(el)[MetadataKind.prop]
const prev = metadata[key]
metadata[key] = value
return prev
}
从这里可以看出,元数据直接注册到元素的一个名为 $$metadata
的属性中。它具有以下类型。
ts
export enum MetadataKind {
prop,
event,
}
export type ComponentMetadata = [
props: Data,
events: Record<string, DelegatedHandler[]>,
]
它似乎有事件处理程序和 Props。
换句话说,它不是直接在这里注册事件处理程序,而只是持有处理程序,实际上,当由 delegateEvents
在 document
中注册的处理程序被调用时,它参考这个元数据来执行处理程序。
on
现在,当 IR
中的 delegate
标志未设置时,调用 on
。
这非常简单,因为它使用 queuePostFlushCb
调用 addEventListener
。
ts
export function on(
el: Element,
event: string,
handlerGetter: () => undefined | ((...args: any[]) => any),
options: AddEventListenerOptions &
ModifierOptions & { effect?: boolean } = {},
): void {
const handler: DelegatedHandler = eventHandler(handlerGetter, options)
let cleanupEvent: (() => void) | undefined
queuePostFlushCb(() => {
cleanupEvent = addEventListener(el, event, handler, options)
})
if (options.effect) {
onEffectCleanup(cleanup)
} else if (getCurrentScope()) {
onScopeDispose(cleanup)
}
function cleanup() {
cleanupEvent && cleanupEvent()
}
}
ts
export function addEventListener(
el: Element,
event: string,
handler: (...args: any) => any,
options?: AddEventListenerOptions,
) {
el.addEventListener(event, handler, options)
return (): void => el.removeEventListener(event, handler, options)
}
清理处理也已实现。
delegate vs on
现在,我们理解了每个实现的差异,但为什么它们被不同地使用?
从 Solid 的文档中可以看出,在 delegate
的情况下,它似乎节省了资源。
更具体地解释一下,"将事件附加到元素"的行为会产生成本(时间和内存)。
虽然 on
单独地将事件附加到每个元素,但 delegate
只将事件附加到文档,当事件发生时,确定事件源自哪个元素,并在相应的元素上触发事件。
这似乎有助于性能。(我自己没有进行基准测试,所以我不知道它在 Vapor 中的效果如何。如果你知道,请告诉我。)
v-bind 指令
现在,让我们继续阅读和进步。
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const dynamicData = ref("a");
</script>
<template>
<div :data-dynamic="dynamicData">Hello, World!</div>
</template>
编译结果和概述
编译结果如下。(我们已经习惯了这个,所以解释变得有点粗略(笑))。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const dynamicData = ref("a");
const __returned__ = { dynamicData, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
renderEffect as _renderEffect,
setDynamicProp as _setDynamicProp,
template as _template,
} from "vue/vapor";
const t0 = _template("<div>Hello, World!</div>");
function _sfc_render(_ctx) {
const n0 = t0();
_renderEffect(() => _setDynamicProp(n0, "data-dynamic", _ctx.dynamicData));
return n0;
}
特别是,
js
function _sfc_render(_ctx) {
const n0 = t0();
_renderEffect(() => _setDynamicProp(n0, "data-dynamic", _ctx.dynamicData));
return n0;
}
是 _setDynamicProp
部分。
正如预期的那样,您现在可以预测实现方法。
总之,这是一个效果,将 _ctx.dynamicData
设置为 n0
的 data-dynamic
属性,这是立即可以理解的。
阅读编译器
熟悉的路线:transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVBind
。
packages/compiler-vapor/src/transforms/vBind.ts
...是这样吗?
实际上,这只处理简写并真正转换 v-bind
,而不注册效果等。
事实上,关于这一点,它直接在 transformElement
的 buildProps
中实现。
实现在这里附近。
ts
context.registerEffect(
[prop.exp],
{
type: IRNodeTypes.SET_DYNAMIC_EVENTS,
element: context.reference(),
event: prop.exp,
},
)
再往上一点,还有当 v-bind
没有 arg
(例如,v-bind="obj"
)时的处理。
ts
if (prop.type === NodeTypes.DIRECTIVE && !prop.arg) {
if (prop.name === 'bind') {
// v-bind="obj"
if (prop.exp) {
dynamicExpr.push(prop.exp)
pushMergeArg()
dynamicArgs.push({
kind: IRDynamicPropsKind.EXPRESSION,
value: prop.exp,
})
} else {
无论如何,既然我们能够看到 SET_DYNAMIC_EVENTS
注册的地方,那就没问题了。
让我们也按原样阅读代码生成。
ts
export function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
ts
case IRNodeTypes.SET_DYNAMIC_PROPS:
return genDynamicProps(oper, context)
ts
export function genDynamicProps(
oper: SetDynamicPropsIRNode,
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
return [
NEWLINE,
...genCall(
vaporHelper('setDynamicProps'),
`n${oper.element}`,
...oper.props.map(
props =>
Array.isArray(props)
? genLiteralObjectProps(props, context) // static and dynamic arg props
: props.kind === IRDynamicPropsKind.ATTRIBUTE
? genLiteralObjectProps([props], context) // dynamic arg props
: genExpression(props.value, context), // v-bind=""
),
),
]
}
应该没有特别困难的部分。
阅读运行时
这里也几乎没有什么可读的。
当 key
是 "class"
或 "style"
时,它只做一点格式化。

v-model 指令
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const text = ref("");
</script>
<template>
<input v-model="text" />
</template>
编译结果和概述
编译结果如下。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const text = ref("");
const __returned__ = { text, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
vModelText as _vModelText,
withDirectives as _withDirectives,
delegate as _delegate,
template as _template,
} from "vue/vapor";
const t0 = _template("<input>");
function _sfc_render(_ctx) {
const n0 = t0();
_withDirectives(n0, [[_vModelText, () => _ctx.text]]);
_delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event));
return n0;
}
最突出的是
js
_withDirectives(n0, [[_vModelText, () => _ctx.text]]);
_delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event));
由于 delegate
再次出现,它在某种程度上表明这是用于注册事件处理程序的,但出现了像 withDirectives
和 _vModelText
这样的神秘元素。
虽然稍后会阅读详细信息,但让我们先阅读编译器。
阅读编译器
遵循路径:transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVModel
。
packages/compiler-vapor/src/transforms/vModel.ts 首先,从 context
中提取 bindingMetadata
。
ts
const bindingType = context.options.bindingMetadata[rawExp]
这是由 compiler-sfc
收集的,包含有关 SFC 中定义的变量的元数据,例如是否在 setup
中定义了 let
变量,或者它是否是 prop、data 等。
具体来说,它枚举如下。
ts
export enum BindingTypes {
/**
* returned from data()
*/
DATA = 'data',
/**
* declared as a prop
*/
PROPS = 'props',
/**
* a local alias of a `<script setup>` destructured prop.
* the original is stored in __propsAliases of the bindingMetadata object.
*/
PROPS_ALIASED = 'props-aliased',
/**
* a let binding (may or may not be a ref)
*/
SETUP_LET = 'setup-let',
/**
* a const binding that can never be a ref.
* these bindings don't need `unref()` calls when processed in inlined
* template expressions.
*/
SETUP_CONST = 'setup-const',
/**
* a const binding that does not need `unref()`, but may be mutated.
*/
SETUP_REACTIVE_CONST = 'setup-reactive-const',
/**
* a const binding that may be a ref.
*/
SETUP_MAYBE_REF = 'setup-maybe-ref',
/**
* bindings that are guaranteed to be refs
*/
SETUP_REF = 'setup-ref',
/**
* declared by other options, e.g. computed, inject
*/
OPTIONS = 'options',
/**
* a literal constant, e.g. 'foo', 1, true
*/
LITERAL_CONST = 'literal-const',
}
如何收集它将在其他地方追踪。
如果 exp
的 bindingType
是 props,则抛出错误。非常周到。
ts
if (
bindingType === BindingTypes.PROPS ||
bindingType === BindingTypes.PROPS_ALIASED
) {
context.options.onError(
createCompilerError(ErrorCodes.X_V_MODEL_ON_PROPS, exp.loc),
)
return
}
然后,以下分支是主要话题。
ts
if (
首先,如果标签是 input
、textarea
或 select
之一。
在这种情况下,它是 input
,所以它匹配这里。
ts
if (
tag === 'input' ||
tag === 'textarea' ||
tag === 'select' ||
对于 input
,它读取 type
属性并确定 runtimeDirective
。
ts
if (tag === 'input' || isCustomElement) {
const type = findProp(node, 'type')
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
runtimeDirective = 'vModelDynamic'
} else if (type.value) {
switch (type.value.content) {
case 'radio':
runtimeDirective = 'vModelRadio'
break
case 'checkbox':
runtimeDirective = 'vModelCheckbox'
break
case 'file':
runtimeDirective = undefined
context.options.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc,
),
)
break
default:
// text type
__DEV__ && checkDuplicatedValue()
break
}
}
之前输出的 vModelText
似乎是这个变量的初始值。
ts
let runtimeDirective: VaporHelper | undefined = 'vModelText'
此时,它注册了一个名为 SET_MODEL_VALUE
的操作,并且
ts
context.registerOperation({
type: IRNodeTypes.SET_MODEL_VALUE,
element: context.reference(),
key: arg || createSimpleExpression('modelValue', true),
value: exp,
isComponent,
})
使用之前计算的 runtimeDirective
注册 withDirectives
。
ts
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
element: context.reference(),
dir,
name: runtimeDirective,
builtin: true,
})
它出奇地简单。
至于代码生成,一旦到达这一点,就很轻松了。
这是常规流程。不需要特别解释。
ts
export function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
ts
case IRNodeTypes.SET_MODEL_VALUE:
return genSetModelValue(oper, context)
ts
export function genSetModelValue(
oper: SetModelValueIRNode,
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
const name = oper.key.isStatic
? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
: ['`update:${', ...genExpression(oper.key, context), '}`']
const handler = genModelHandler(oper.value, context)
return [
NEWLINE,
...genCall(vaporHelper('delegate'), `n${oper.element}`, name, handler),
]
}
export function genModelHandler(
value: SimpleExpressionNode,
context: CodegenContext,
): CodeFragment[] {
const {
options: { isTS },
} = context
return [
`() => ${isTS ? `($event: any)` : `$event`} => (`,
...genExpression(value, context, '$event'),
')',
]
}
withDirectives
部分有一个稍微不同的代码生成流程。
它遵循 genBlockContent
-> genChildren
-> genDirectivesForElement
-> genWithDirective
。
ts
export function genBlockContent(
block: BlockIRNode,
context: CodegenContext,
root?: boolean,
customReturns?: (returns: CodeFragment[]) => CodeFragment[],
): CodeFragment[] {
↓
ts
for (const child of dynamic.children) {
push(...genChildren(child, context, child.id!))
}
↓
ts
export function genChildren(
dynamic: IRDynamicInfo,
context: CodegenContext,
from: number,
paths: number[] = [],
): CodeFragment[] {
↓
ts
push(...genDirectivesForElement(id, context))
或
ts
push(...genDirectivesForElement(id, context))
↓
ts
export function genDirectivesForElement(
id: number,
context: CodegenContext,
): CodeFragment[] {
const dirs = filterDirectives(id, context.block.operation)
return dirs.length ? genWithDirective(dirs, context) : []
}
↓
ts
export function genWithDirective(
opers: WithDirectiveIRNode[],
context: CodegenContext,
): CodeFragment[] {
是路径。
阅读运行时
虽然 _delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event));
部分没问题,但问题在于 withDirectives
和 _vModelText
。
withDirectives
让我们阅读 withDirectives
。 实现在 packages/runtime-vapor/src/directives.ts 中。
ts
export function withDirectives<T extends ComponentInternalInstance | Node>(
nodeOrComponent: T,
directives: DirectiveArguments,
): T {
withDirectives
接收 node
或 component
,以及 directives
。
DirectiveArguments
DirectiveArguments
的定义如下。
ts
export type DirectiveArguments = Array<
| [Directive | undefined]
| [Directive | undefined, () => any]
| [Directive | undefined, () => any, argument: string]
| [
Directive | undefined,
value: () => any,
argument: string,
modifiers: DirectiveModifiers,
]
>
ts
export type Directive<T = any, V = any, M extends string = string> =
| ObjectDirective<T, V, M>
| FunctionDirective<T, V, M>
ts
export type FunctionDirective<
T = any,
V = any,
M extends string = string,
> = DirectiveHook<T, V, M>
ts
export type ObjectDirective<T = any, V = any, M extends string = string> = {
[K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
} & {
/** Watch value deeply */
deep?: boolean | number
}
ts
export type DirectiveHook<
T = any | null,
V = any,
M extends string = string,
> = (node: T, binding: DirectiveBinding<T, V, M>) => void
// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
// effect update -> `beforeUpdate` -> node updated -> `updated`
// `beforeUnmount`-> node unmount -> `unmounted`
export type DirectiveHookName =
| 'created'
| 'beforeMount'
| 'mounted'
| 'beforeUpdate'
| 'updated'
| 'beforeUnmount'
| 'unmounted'
它有点复杂,但简单来说,它为每个生命周期定义了行为。
(这似乎是指令的实际行为。)
withDirectives 内部
首先,有一个称为 DirectiveBinding
的概念。
这是一个捆绑必要信息的对象,例如旧值和新值、修饰符、指令本身(ObjectDirective
),以及在组件的情况下,实例。
ts
export interface DirectiveBinding<T = any, V = any, M extends string = string> {
instance: ComponentInternalInstance
source?: () => V
value: V
oldValue: V | null
arg?: string
modifiers?: DirectiveModifiers<M>
dir: ObjectDirective<T, V, M>
}
然后,这个函数 withDirectives
,顾名思义,可以应用多个指令。
它处理作为参数接收的指令数组中的每个指令。
ts
for (const directive of directives) {
让我们看看在这个 for 循环中做了什么。
首先,从定义中提取各种信息。
ts
let [dir, source, arg, modifiers] = directive
同时,进行规范化。
ts
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir,
} satisfies ObjectDirective
}
定义基本绑定。
ts
const binding: DirectiveBinding = {
dir,
instance,
value: null, // set later
oldValue: undefined,
arg,
modifiers,
}
用 ReactiveEffect
包装 source
,并设置该 effect
的 scheduler
以包含更新触发器。
ts
if (source) {
if (dir.deep) {
const deep = dir.deep === true ? undefined : dir.deep
const baseSource = source
source = () => traverse(baseSource(), deep)
}
const effect = new ReactiveEffect(() =>
callWithErrorHandling(
source!,
instance,
VaporErrorCodes.RENDER_FUNCTION,
),
)
const triggerRenderingUpdate = createRenderingUpdateTrigger(
instance,
effect,
)
effect.scheduler = () => queueJob(triggerRenderingUpdate)
binding.source = effect.run.bind(effect)
}
更新触发器只是一个执行生命周期的 beforeUpdate
和 updated
的触发器。
ts
export function createRenderingUpdateTrigger(
instance: ComponentInternalInstance,
effect: ReactiveEffect,
): SchedulerJob {
job.id = instance.uid
return job
function job() {
if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
return
}
if (instance.isMounted && !instance.isUpdating) {
instance.isUpdating = true
const reset = setCurrentInstance(instance)
const { bu, u, scope } = instance
const { dirs } = scope
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
invokeDirectiveHook(instance, 'beforeUpdate', scope)
queuePostFlushCb(() => {
instance.isUpdating = false
const reset = setCurrentInstance(instance)
if (dirs) {
invokeDirectiveHook(instance, 'updated', scope)
}
// updated hook
if (u) {
queuePostFlushCb(u)
}
reset()
})
reset()
}
}
}
最后,执行创建钩子。
ts
callDirectiveHook(node, binding, instance, 'created')
vModelText
现在我们已经读到这里,接下来让我们阅读特定指令的实现。
与 v-model 相关的 runtimeDirective
在 packages/runtime-vapor/src/directives/vModel.ts 中实现。
这次,vModelText
如下。
ts
export const vModelText: ObjectDirective<
HTMLInputElement | HTMLTextAreaElement,
any,
'lazy' | 'trim' | 'number'
> = {
在这里,定义了与此指令相关的每个生命周期的行为,例如 beforeMount
、mounted
、beforeUpdate
等。 让我们逐一查看。
beforeMount
ts
beforeMount(el, { modifiers: { lazy, trim, number } = {} }) {
注册事件处理程序。
在更新值时执行值的修剪和转换为数字。
ts
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
domValue = domValue.trim()
}
if (castToNumber) {
domValue = looseToNumber(domValue)
}
assigner(domValue)
})
值更新使用从委托事件处理程序获得的赋值器。
ts
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
domValue = domValue.trim()
}
if (castToNumber) {
domValue = looseToNumber(domValue)
}
assigner(domValue)
})
ts
function getModelAssigner(el: Element): AssignerFn {
const metadata = getMetadata(el)
const fn = metadata[MetadataKind.event]['update:modelValue'] || []
return value => invokeArrayFns(fn, value)
}
虽然官方文档提到 v-model 处理诸如 IME 之类的组合,但这正是这个过程。
mounted
在挂载时,它只是设置初始值。
ts
mounted(el, { value }) {
el.value = value == null ? '' : value
},
beforeUpdate
处理诸如 IME 之类的组合,并跳过不必要的更新直到更新。
ts
beforeUpdate(el, { value, modifiers: { lazy, trim, number } = {} }) {
assignFnMap.set(el, getModelAssigner(el))
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
const elValue =
number || el.type === 'number' ? looseToNumber(el.value) : el.value
const newValue = value == null ? '' : value
if (elValue === newValue) {
return
}
// eslint-disable-next-line no-restricted-globals
if (document.activeElement === el && el.type !== 'range') {
if (lazy) {
return
}
if (trim && el.value.trim() === newValue) {
return
}
}
el.value = newValue
},
有了这个,v-model
的操作应该被理解了。
除了 vModelText
之外,还有各种其他指令定义,但您应该能够基于此以相同的方式进行。
而且,由于这次出现了诸如 runtimeDirective
和 withDirectives
之类的新概念,所以变得有点冗长,但您应该能够基于此继续阅读其他指令。(您的速度也应该会提高。)
让我们以这种方式继续阅读。
v-show 指令
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const flag = ref("");
</script>
<template>
<p v-show="flag">Hello, v-show!</p>
</template>
编译结果和概述
编译结果如下。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const flag = ref("");
const __returned__ = { flag, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
vShow as _vShow,
withDirectives as _withDirectives,
template as _template,
} from "vue/vapor";
const t0 = _template("<p>Hello, v-show!</p>");
function _sfc_render(_ctx) {
const n0 = t0();
_withDirectives(n0, [[_vShow, () => _ctx.flag]]);
return n0;
}
突出的是 _withDirectives(n0, [[_vShow, () => _ctx.flag]]);
部分。
继续上次的内容,它再次使用 withDirectives
函数,但这次它使用 vShow
函数作为运行时指令。
阅读编译器
我们将遵循 transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVShow
。
它非常简单,所以我将包含整个文本。
ts
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'
export const transformVShow: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
)
}
context.registerOperation({
type: IRNodeTypes.WITH_DIRECTIVE,
element: context.reference(),
dir,
name: 'vShow',
builtin: true,
})
}
它只是注册带有 name: 'vShow'
的 WITH_DIRECTIVE
。
阅读运行时
这也很简单,所以我将包含整个文本。
它只是在 beforeMount
、updated
和 beforeUnmount
中将 el.style.display
设置为 none
或 ""
。
ts
import type { ObjectDirective } from '../directives'
const vShowMap = new WeakMap<HTMLElement, string>()
export const vShow: ObjectDirective<HTMLElement> = {
beforeMount(node, { value }) {
vShowMap.set(node, node.style.display === 'none' ? '' : node.style.display)
setDisplay(node, value)
},
updated(node, { value, oldValue }) {
if (!value === !oldValue) return
setDisplay(node, value)
},
beforeUnmount(node, { value }) {
setDisplay(node, value)
},
}
function setDisplay(el: HTMLElement, value: unknown): void {
el.style.display = value ? vShowMap.get(el)! : 'none'
}
这就是全部内容!
v-once 指令
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<p v-once>{{ count }}</p>
</template>
编译结果和概述
编译结果如下。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const count = ref(0);
const __returned__ = { count, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import { setText as _setText, template as _template } from "vue/vapor";
const t0 = _template("<p></p>");
function _sfc_render(_ctx) {
const n0 = t0();
_setText(n0, _ctx.count);
return n0;
}
突出的是 setText
部分没有被包装在 renderEffect
中。
由于 v-once
只渲染一次,所以不需要将其包装在 renderEffect
中。
阅读编译器
我们将遵循 transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVOnce
。
它非常简单,所以我将包含整个文本。
ts
import { NodeTypes, findDir } from '@vue/compiler-dom'
import type { NodeTransform } from '../transform'
export const transformVOnce: NodeTransform = (node, context) => {
if (
// !context.inSSR &&
node.type === NodeTypes.ELEMENT &&
findDir(node, 'once', true)
) {
context.inVOnce = true
}
}
它只是启用 context
持有的 inVOnce
标志。
当 inVOnce
为 true 时,它调用带有 registerEffect
的 registerOperation
并完成,这意味着不生成效果。
ts
registerEffect(
expressions: SimpleExpressionNode[],
...operations: OperationNode[]
): void {
expressions = expressions.filter(exp => !isConstantExpression(exp))
if (this.inVOnce || expressions.length === 0) {
return this.registerOperation(...operations)
}
由于运行时中没有特别需要阅读的内容,所以暂时就到这里。
v-text 指令
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const message = ref("Hello, v-text!");
</script>
<template>
<p v-text="message" />
</template>
编译结果和概述
编译结果如下。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const message = ref("Hello, v-text!");
const __returned__ = { message, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
renderEffect as _renderEffect,
setText as _setText,
template as _template,
} from "vue/vapor";
const t0 = _template("<p></p>");
function _sfc_render(_ctx) {
const n0 = t0();
_renderEffect(() => _setText(n0, _ctx.message));
return n0;
}
使用 v-text
指定的元素是使用 renderEffect
和 setText
构建的。
阅读编译器
我们将遵循路径 transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVText
。
它非常简单,所以我将包含整个文本。
ts
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import { IRNodeTypes } from '../ir'
import { EMPTY_EXPRESSION } from './utils'
import type { DirectiveTransform } from '../transform'
import { getLiteralExpressionValue } from '../utils'
export const transformVText: DirectiveTransform = (dir, node, context) => {
let { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
)
exp = EMPTY_EXPRESSION
}
if (node.children.length) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc),
)
context.childrenTemplate.length = 0
}
const literal = getLiteralExpressionValue(exp)
if (literal != null) {
context.childrenTemplate = [String(literal)]
} else {
context.registerEffect([exp], {
type: IRNodeTypes.SET_TEXT,
element: context.reference(),
values: [exp],
})
}
}
它只是注册 registerEffect
。
它执行类似于调用 transformText
时的处理。
在运行时中没有特别需要阅读的内容,所以暂时就到这里。
v-html 指令
考虑以下组件。
vue
<script setup>
import { ref } from "vue";
const inner = ref("<p>Hello, v-html</p>");
</script>
<template>
<div v-html="inner" />
</template>
编译结果和概述
编译结果如下。
js
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const inner = ref("<p>Hello, v-html</p>");
const __returned__ = { inner, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
import {
renderEffect as _renderEffect,
setHtml as _setHtml,
template as _template,
} from "vue/vapor";
const t0 = _template("<div></div>");
function _sfc_render(_ctx) {
const n0 = t0();
_renderEffect(() => _setHtml(n0, _ctx.inner));
return n0;
}
出现了一个名为 setHtml
的新辅助函数。
它的实现并没有特别改变。
阅读编译器
我们将遵循路径 transformElement
-> buildProps
-> transformProps
-> directiveTransform
-> transformVHtml
。
它非常简单,所以我将包含整个文本。
ts
import { IRNodeTypes } from '../ir'
import type { DirectiveTransform } from '../transform'
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import { EMPTY_EXPRESSION } from './utils'
export const transformVHtml: DirectiveTransform = (dir, node, context) => {
let { exp, loc } = dir
if (!exp) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
)
exp = EMPTY_EXPRESSION
}
if (node.children.length) {
context.options.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
)
context.childrenTemplate.length = 0
}
context.registerEffect([exp], {
type: IRNodeTypes.SET_HTML,
element: context.reference(),
value: exp,
})
}
它只是用 SET_HTML
注册一个效果。
它执行类似于调用 transformText
时的处理。
让我们看看代码生成。
ts
export function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
↓
ts
case IRNodeTypes.SET_HTML:
return genSetHtml(oper, context)
↓
ts
export function genSetHtml(
oper: SetHtmlIRNode,
context: CodegenContext,
): CodeFragment[] {
const { vaporHelper } = context
return [
NEWLINE,
...genCall(
vaporHelper('setHtml'),
`n${oper.element}`,
genExpression(oper.value, context),
),
]
}
这很熟悉。
阅读运行时
让我们只读 setHtml
。
ts
export function setHtml(el: Element, value: any): void {
const oldVal = recordPropMetadata(el, 'innerHTML', value)
if (value !== oldVal) {
el.innerHTML = value
}
}
它非常简单,只是设置 innerHTML
。
目前,它似乎从元信息中提取旧值以防止不必要的设置。
最后
这本书还有部分内容没有完成,后续若作者将内容完善后,我也会把本文译文给补全,翻译不易,喜欢还请给个赞支持下