前言#
昨天我们分析了处理特殊标签以及inline style,
坏蛋Dan:vue/compiler-dom源码分析学习--day2: 处理特殊标签&副作用标签
今天我们继续往下分析,该轮到指令了。
before分析#
先来看下分析顺序
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn, // override compiler-core
show: transformShow
}我们就按照这个顺序来分析。
v-cloak#
我们的代码中没有这个指令,所以要补充下


// - DirectiveTransform:
// Transforms that handles a single directive attribute on an element.
// It translates the raw directive into actual props for the VNode.
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext,
// a platform specific compiler can import the base transform and augment
// it by passing in this optional argument.
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
) => DirectiveTransformResult
export interface DirectiveTransformResult {
props: Property[]
needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
}
export const noopDirectiveTransform: DirectiveTransform = () => ({ props: [] })由于代码只有一行,所以我把type也放进来了。
可以看到指令最终会被转换成属性,而v-cloak这个指令。。。有点尴尬,也不知道后面咋回事。。。
我们暂时mark下先去分析别的了。
不过这里有一点我们知道了就是directive会被处理成props。
v-html#
我发现我压根都没写任何的指令。。。

import {
DirectiveTransform,
createObjectProperty,
createSimpleExpression
} from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
export const transformVHtml: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc)
)
}
if (node.children.length) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc)
)
node.children.length = 0
}
return {
props: [
createObjectProperty(
createSimpleExpression(`innerHTML`, true, loc),
exp || createSimpleExpression('', true)
)
]
}
}之前有个点貌似忘了说了,那就是这个exp到底是个啥,看名字其实就知道了,是expression表达式,比如这个v-html="html"中的html就是exp。

至于loc相信大家都清楚,就是定位代码位置用的,一般用于sourcemap
这个指令的代码很好理解
- 表达式为空
return - 用了
v-html还想有子节点,有点贪心了,给它忽略掉子节点。 - 创建一个
innerHTML的属性节点

v-text#

这个指令挺少用,因为大部分情况下都是mustache也就是双大括号。
export const transformVText: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc)
)
}
if (node.children.length) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc)
)
node.children.length = 0
}
return {
props: [
createObjectProperty(
createSimpleExpression(`textContent`, true),
exp
? getConstantType(exp, context) > 0
? exp
: createCallExpression(
context.helperString(TO_DISPLAY_STRING),
[exp],
loc
)
: createSimpleExpression('', true)
)
]
}
}这个方法和v-html差不多,都是不允许有子节点的存在。
这里有调用一个getConstantType的方法,我们可以推测下这个方法是用来静态节点提升的,毕竟有constant字眼。
并且传入的不是node,而是exp,这就意味着必定是一个表达式,所以我们就不看getConstantType的具体代码了,反正到时候也会遇到,我们就看exp对应类型的那一段

对应的ast的type是4,而constantType我们暂时不清楚是从哪来的,反正传入的node.exp已经自带了

其实我们可以通过getConstantType这个方法的返回值的类型中推测出来主要是用来干嘛的

我们的v-text第一个例子传入的是一个变量,这里拿到的constantType是0,而传入字符串的则是3。
一个是NOT_CONSTANT,一个则是CAN_STRINGIFY。
这里有句注释:Higher levels implies lower levels.
啥意思呢?应该是这个枚举中对应的数字越高越“安静”越稳定不变。 都能直接stringify了,那就说明完全不可变了,相反not constant则表示可变。
如果是not constant的话,会变成下面这样

createCallExpression: 看名字就知道是创建一个call expression,callee则是调用的函数名字
所以说这个表达式最终应该是((_ctx) => _toDisplayString(_ctx.text))。
v-model#
这块相信大家都比较熟悉了
但是在看代码之前,得先知道一种用法,给子组件绑定v-model的场景[5]

相当于props+ emit
你可以在这里试一下,有一点需要注意的是vue3.x里子组件接收的对应的prop得是modelValue这个名字(默认,可自行定义),不然就无法双向。

import {
transformModel as baseTransform,
DirectiveTransform,
ElementTypes,
findProp,
NodeTypes,
hasDynamicKeyVBind
} from '@vue/compiler-core'
import { createDOMCompilerError, DOMErrorCodes } from '../errors'
import {
V_MODEL_CHECKBOX,
V_MODEL_RADIO,
V_MODEL_SELECT,
V_MODEL_TEXT,
V_MODEL_DYNAMIC
} from '../runtimeHelpers'
export const transformModel: DirectiveTransform = (dir, node, context) => {
const baseResult = baseTransform(dir, node, context)
// base transform has errors OR component v-model (only need props)
if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult
}
if (dir.arg) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
dir.arg.loc
)
)
}
function checkDuplicatedValue() {
const value = findProp(node, 'value')
if (value) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
value.loc
)
)
}
}
const { tag } = node
const isCustomElement = context.isCustomElement(tag)
if (
tag === 'input' ||
tag === 'textarea' ||
tag === 'select' ||
isCustomElement
) {
let directiveToUse = V_MODEL_TEXT
let isInvalidType = false
if (tag === 'input' || isCustomElement) {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc
)
)
break
default:
// text type
__DEV__ && checkDuplicatedValue()
break
}
}
} else if (hasDynamicKeyVBind(node)) {
// element has bindings with dynamic keys, which can possibly contain
// "type".
directiveToUse = V_MODEL_DYNAMIC
} else {
// text type
__DEV__ && checkDuplicatedValue()
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
} else {
// textarea
__DEV__ && checkDuplicatedValue()
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
} else {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
dir.loc
)
)
}
// native vmodel doesn't need the `modelValue` props since they are also
// passed to the runtime as `binding.value`. removing it reduces code size.
baseResult.props = baseResult.props.filter(
p =>
!(
p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
p.key.content === 'modelValue'
)
)
return baseResult
}代码有点长
transformModel as baseTransform,: 也就是说@vue/compiler-core包里其实有转换model的方法,但是这里又重写了。
这个baseTransform咱们先不看,先看这个transformModel里做了什么
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT: 这个报错看下对应的文本(突然想起前面几个都没有看错误提示文案。。。不过没差,都认识是啥报错)
v-model argument is not supported on plain elements.也就是你这个v-model指令不能含有argument,那是啥情况呢?实际上是这个情况

不需要我们再去定这个value,因为这个是默认的。
V_MODEL_TEXT: 表示当前v-model绑定的类型type

为什么要确定类型呢?因为v-model只是看起来很方便,实际上没那么神奇,只是判断类型做不同转换,脏活累活都交给编译器了。
V_MODEL_DYNAMIC:表示这个type是动态的,比如:type="type",现在还不能确定是啥,只能等runtime了。噢,前面有个点忘了说了,你使用的:xxx,这个:大家应该都记得是语法糖吧,所以在编译的时候等价于v-on:xxxX_V_MODEL_ON_FILE_INPUT_ELEMENT: 来看下错误提示文案:v-model cannot be used on file inputs since they are read-only. Use a v-on:change listener instead.这玩意儿不支持file类型,用@change替换。checkDuplicatedValue: 除了radio、checkbox之外,能用v-model的是类似text的类型,比如number,而这个时候,并不需要我们去给你的节点绑定一个value属性,因为v-model默认找的就是value,而radio和checkbox需要多个节点配合,所以需要确定value。hasDynamicKeyVBind: 这个方法代码就不看了,简单的说就是判断你这个节点动态绑定的属性里有没有type这个属性,比如这种v-bind="{ type: 'checkbox' }",不过这段得交由runtime去处理了,给它定为dynamiccontext.helper(directiveToUse): 看到helper就大概率是runtime相关的,这块实际上是注入runtime需要的方法,通过directiveToUse这个key确定下来的类型来判断注入哪个runtime方法(helper)。

从这里你应该也能想到一些render function如何生成的逻辑,比如这块import helper,其它情况也都差不多。
最后做了一层filter操作,把value="modelValue"这个props给去掉了,为什么呢?因为它们(非组件)不需要,这个modelValue是vue3.x提供给组件使用v-model的,所以其它都不需要,而需要的组件早就被return掉了。
if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult
} 组件不需要runtime的helper,因为组件使用v-model是props + emit的语法糖。
而baseTransform我放到其它这一章节里。
有一点需要注意,那就是上面的代码不会涉及到components。
v-on但是是@vue/compiler-core里的#
分析了一大半突然发现分析错包了。。。。。。。。。。。。。。。。。。。。。。。。 算了,这个就将错就错了,我说量怎么这么大呢。。。不过好在@vue/compiler-dom里的这个vOn是基于这个core包里的vOn的,也得分析。
这个相信大家都耳熟能详了,不过大部分情况下我们都是使用语法糖写法@xxx。
在开始分析代码之前,我们需要知道有几种用法,这样才能方便我们分析
<!-- method handler -->
<button v-on:click="doThis"></button>
<!-- dynamic event -->
<button v-on:[event]="doThis"></button>
<!-- inline statement -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- shorthand -->
<button @click="doThis"></button>
<!-- shorthand dynamic event -->
<button @[event]="doThis"></button>
<!-- stop propagation -->
<button @click.stop="doThis"></button>
<!-- prevent default -->
<button @click.prevent="doThis"></button>
<!-- prevent default without expression -->
<form @submit.prevent></form>
<!-- chain modifiers -->
<button @click.stop.prevent="doThis"></button>
<!-- key modifier using keyAlias -->
<input @keyup.enter="onEnter" />
<!-- the click event will be triggered at most once -->
<button v-on:click.once="doThis"></button>
<!-- object syntax -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>把这些一股脑全给贴到我们的team.vue里。
ok,现在可以来看代码了
export const transformOn: DirectiveTransform = (
dir,
node,
context,
augmentor
) => {
const { loc, modifiers, arg } = dir as VOnDirectiveNode
if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
}
let eventName: ExpressionNode
if (arg.type === NodeTypes.SIMPLE_EXPRESSION) {
if (arg.isStatic) {
let rawName = arg.content
// TODO deprecate @vnodeXXX usage
if (rawName.startsWith('vue:')) {
rawName = `vnode-${rawName.slice(4)}`
}
const eventString =
node.tagType === ElementTypes.COMPONENT ||
rawName.startsWith('vnode') ||
!/[A-Z]/.test(rawName)
? // for component and vnode lifecycle event listeners, auto convert
// it to camelCase. See issue #2249
toHandlerKey(camelize(rawName))
: // preserve case for plain element listeners that have uppercase
// letters, as these may be custom elements' custom events
`on:${rawName}`
eventName = createSimpleExpression(eventString, true, arg.loc)
} else {
// #2388
eventName = createCompoundExpression([
`${context.helperString(TO_HANDLER_KEY)}(`,
arg,
`)`
])
}
} else {
// already a compound expression.
eventName = arg
eventName.children.unshift(`${context.helperString(TO_HANDLER_KEY)}(`)
eventName.children.push(`)`)
}
// handler processing
let exp: ExpressionNode | undefined = dir.exp as
| SimpleExpressionNode
| undefined
if (exp && !exp.content.trim()) {
exp = undefined
}
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
if (exp) {
const isMemberExp = isMemberExpression(exp.content, context)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) {
isInlineStatement && context.addIdentifiers(`$event`)
exp = dir.exp = processExpression(
exp,
context,
false,
hasMultipleStatements
)
isInlineStatement && context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference
// to scope variables.
shouldCache =
context.cacheHandlers &&
// unnecessary to cache inside v-once
!context.inVOnce &&
// runtime constants don't need to be cached
// (this is analyzed by compileScript in SFC <script setup>)
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.constType > 0) &&
// #1541 bail if this is a member exp handler passed to a component -
// we need to use the original function to preserve arity,
// e.g. <transition> relies on checking cb.length to determine
// transition end handling. Inline function is ok since its arity
// is preserved even when cached.
!(isMemberExp && node.tagType === ElementTypes.COMPONENT) &&
// bail if the function references closure variables (v-for, v-slot)
// it must be passed fresh to avoid stale values.
!hasScopeRef(exp, context.identifiers)
// If the expression is optimizable and is a member expression pointing
// to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched.
if (shouldCache && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content = `${exp.content} && ${exp.content}(...args)`
} else {
exp.children = [...exp.children, ` && `, ...exp.children, `(...args)`]
}
}
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
exp as SimpleExpressionNode,
context,
false,
hasMultipleStatements
)
}
if (isInlineStatement || (shouldCache && isMemberExp)) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`${
isInlineStatement
? !__BROWSER__ && context.isTS
? `($event: any)`
: `$event`
: `${
!__BROWSER__ && context.isTS ? `\n//@ts-ignore\n` : ``
}(...args)`
} => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`
])
}
}
let ret: DirectiveTransformResult = {
props: [
createObjectProperty(
eventName,
exp || createSimpleExpression(`() => {}`, false, loc)
)
]
}
// apply extended compiler augmentor
if (augmentor) {
ret = augmentor(ret)
}
if (shouldCache) {
// cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline handlers on
// components.
ret.props[0].value = context.cache(ret.props[0].value)
}
// mark the key as handler for props normalization check
ret.props.forEach(p => (p.key.isHandlerKey = true))
return ret
} 代码也是有些长
dir:directive,不多说。arg:v-on绑定的变量。exp: 表达式。modifier: 修饰符。X_V_ON_NO_EXPRESSION: 这个应该不用多说,直接看下错误文案即可。v-on is missing expression.不过有一点需要注意,那就是有的场景并不需要表达式,有个修饰符就可以了,比如@submit.prevent就不需要传入表达式。isStatic: 也是老朋友字段了,如果你是这样的:@[event],那就是false。vue:这个是个啥玩意儿呢?官方文档(其实我一直在api文档里找。。。)里我找了半天都没找到,最后跟着git记录搜索了下,发现了这个迁移文档

原来是用来监听自组件的生命周期的,vue2中是hook:,3.x改成vue:并且能监听html元素,太得劲了
// 2.x
<template>
<child-component @hook:updated="onUpdated">
</template>
// 3.x
<template>
<child-component @vue:updated="onUpdated">
</template>camelize: 看名字就是帮你把-xxx转换成大写X
const camelizeRE = /-(\w)/g
/**
* @private
*/
export const camelize = cacheStringFunction((str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
}) toHandlerKey: 有点套,结合上面的camelize整理一下。
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
const cache: Record<string, string> = Object.create(null)
return ((str: string) => {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}) as T
}
/**
* @private
*/
export const capitalize = cacheStringFunction(
(str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)
/**
* @private
*/
export const toHandlerKey = cacheStringFunction((str: string) =>
str ? `on${capitalize(str)}` : ``
)toHandlerKey(camelize(rawName))实际上是先将你的横杠小写的规范改成词首字母大写的规范,比如
it-is变成itIs。然后调用toHandlerKey先把你的首字母大写变成ItIs,然后再on包裹,变成onItIs。
而cacheStringFunction是一个高阶函数,里面用到了闭包。接收一个function,返回一个function。
这里有两个东西缓存下来了,一个是cache,一个是fn,每次往里面存放str的时候如果没有在cache里找到对应的值,就调用fn生成并缓存。
那么总结下toHandlerKey这个方法是干啥用的:
- 帮你把首字母大写然后添加前缀
on - 缓存数据。
所以这一段是在整理rawName也就是监听的事件名字
注意这里的条件
node.tagType === ElementTypes.COMPONENT ||
rawName.startsWith('vnode') ||
!/[A-Z]/.test(rawName) 组件/上面说到的使用hook:(2.x)或者vue:(3.x,这也就是为什么vue3.x生命周期函数名字改了但是这里使用不需要同步改动的原因)/没有大写的事件名(这就是内置元素的事件名,规范就是全小写字母并且不能用下划线)。
注意这里的组件,我们来看下这一行代码之后的代码
: // preserve case for plain element listeners that have uppercase
// letters, as these may be custom elements' custom events
`on:${rawName}`我把注释也贴进来了,这里有说到custom elements,翻译过来也就是自定义元素。
自定义元素#
这是个啥?不是组件吗?是的,不是。vue允许你去定义一些自定义元素,比如一些浏览器支持的标签但是没有被vue收录的。
Vue and Web Componentsvuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue
vue的编译器在遇到不认识的tag也就是标签的时候默认把它当作是一个组件,而对于自定义元素来说就会引起报错:failed to resolve component。所以vue官方提供了选项,允许过滤掉(不处理,保留)自定义元素。
先来看下vue2里的做法
Vue.config.ignoredElements = ['plastic-button'] 通过Vue.config.ignoreElements来忽略(保留不改动)它们。 但注意,这是在runtime。
vue3.x迁移到了编译阶段(非in-browser),来看下vite的配置
// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
// treat all tags with a dash as custom elements
isCustomElement: (tag) => tag.includes('-')
}
}
})
]
}
或者vue-cli
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => ({
...options,
compilerOptions: {
// treat any tag that starts with ion- as custom elements
isCustomElement: tag => tag.startsWith('ion-')
}
}))
}
}
ok,回到我们的代码中,我们看到自定义元素都不处理,并且加上on:的前缀。
TO_HANDLER_KEY: 实际上就是toHandlerKey这个方法的runtime同名方法
export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``) 为啥这么绕口呢。。。因为这里无法处理dynamic也就是isStatic为false的场景,即@[event]="xxx"这种写法,由于无法确定具体的eventName,所以只能是交由runtime去解析处理。
createCompoundExpression: 这个方法前面见过好几次了,每次都出现在dynamic的场景。对应节点的type为NodeTypes.COMPOUND_EXPRESSION也就是8。- 然后什么情况下不是
v-on绑定的事件名(也就是arg.content)不是NodeTypes.SIMPLE_EXPRESSION也就是type == 4呢?来看下下面这种写法
<button @[`${test}`]="abort"></button> 
这个type == 8自然就是上面NodeTypes.COMPOUND_EXPRESSION,需要runtime的时候才能处理。
上面说完了arg的处理,接下来轮到exp了。
context.inVOnce: 这个应该是.once的操作。MemberExpression[8]: 你应该注意到了,前面好几处出现了,但是都没有说。这玩意儿真的有些难懂,翻译过来叫做成员表达式。。。。这谁懂,然后babel对应的那块逻辑压根没给出例子。没得办法只能上网找大佬们的解释。
**memberExpression**#
这里推荐一下这个网站,每一个词都会有对应的节点展示

这个obj.a()中的obj.a就是一个memberExpression
我们再来看个例子,我把obj.a改成obj.a.b

可以看到,这里出现了两个memberExpression,一个是obj.a,一个是obj.a.b。也就是说这玩意儿是支持套娃的。
然后我们再来看下一个例子

一个数组调用元素就是一个memberExpression,这点又和xx.xx这样的逻辑不同。需要注意,用[]来调用的场景computed会是true,反过来也就是说computed为true,那就是通过[]去读取的方式。
说了这么多,该归纳下了
xx.xx这种和xx[xx]这种又或者xx[xx].xx都是memberExpression。
ok,回到代码中,这里有点想吐槽知乎的章节结构最多两层。。。好歹支持个三层吧。。
isMemberExpression这个方法里做了什么判断就不看了,有点伤脑力,直接看这个名字就知道针对memberExpression这种节点的。isInlineStatement:顾名思义,inlineStatement就是类似SIMPLE_EXPRESSION的样子。fnExpRE: 来看下正则
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/看不大懂没事,关键字认清楚即可async function,() => {}。一个行内function,这个也要归纳到非inlineStatement的场景。
hasMultipleStatements: 很好理解,多条语句,比如下面这种

这样是可行的。另外这也算是inlineStatement。
processExpression: 看名字是在说加工表达式,代码我就放到其它里了,量有些大 。简单的说就是在处理表达式,最终生成一个新的节点。removeIdentifiers: 然后又移除呢?因为你这个表达式可能是一个函数调用表达式然后你的参数里有个$event,比如@click="a($event)",这个时候就需要补上$event作为默认参数。至于为啥要特意在这个processExpression的阶段补上这个$event到context.identifier呢?因为这个表达式会被拆分为a和$event,这个$event如果分析的时候不认识就会被当作_ctx.$event,所以这里就需要给加上。至于之后又移除,这自然是避免污染上下文变量集合context.identifier。shouldCache一看就是用来判断是否可以缓存的标志位的,不过有两种判断条件。如果没有exp也就是没有表达式,那么只需要存在cacheHandlers和没有使用.once修饰符。毕竟.once只执行一次没必要缓存。而如果有表达式,那么缓存条件就有些复杂了。有cacheHandlers和非.once这两个是必然的,我们来看下在有表达式的场景下还有哪些条件
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.constType > 0)这个条件就有点怪了,看下注释runtime constants don't need to be cached.(this is analyzed by compileScript in SFC <script setup>)。也就是说runtime的const变量不需要缓存处理,它们由compileScript控制。怪了,我们之前虽然没有分析,但是cache实际上只有在import的阶段有,难不成是bindingMetadata.先mark吧。!(isMemberExp && node.tagType === ElementTypes.COMPONENT)如果是组件,并且表达式是一个比如a.b或者a['b']这种。这样也不缓存,为什么呢?因为它的源头可能不见了,比如a.b里的a挂了,这个时候如果还在缓存阶段,就有问题了。所以需要整个引用源都保护起来。!hasScopeRef(exp, context.identifiers)这个比较好理解,不能引用v-for/v-slot里的变量,为什么呢?因为这样产生闭包,到时候容易存在内存泄漏。
augmentor: 这个就是@vue/compiler-dom这个包里的vOn传入的回调,我们等会再回去看下。
总结一下
- 处理等号左边的绑定事件的名字/变量,转换为一个
eventName的节点,具体请往回看。 - 处理等号右边的表达式,转换为一个
exp的节点,具体请往回看。 - 给可以缓存的表达式包裹一层
($event/...args) => { exp } - 创建一个
objectProperty的节点,key就是第一点里的节点,value则是第二点表达式的节点。 - 调用
@vue/compiler-dom里的vOn.ts传入的回调(待分析) - 如果需要缓存,将第四点的
value做一层缓存处理,这样数据就不会发生变化,这样能避免组件的重复渲染。 - 标记这个属性的
key为isHandlerKey,表示这是一个handler,到时候需要执行。 - 返回这个
props
来看下最终数据

而我的原始代码是

v-on#
上面终于分析完了@vue/compiler-core包里的v-on,也就是@vue/compiler-dom的vOn.ts里的baseTransform,但是还留了一个argumetor回调没有分析,而这个回调就在@vue/compiler-dom的vOn.ts中。
export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransform(dir, node, context, baseResult => {
const { modifiers } = dir
if (!modifiers.length) return baseResult
let { key, value: handlerExp } = baseResult.props[0]
const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
resolveModifiers(key, modifiers, context, dir.loc)
// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('right')) {
key = transformClick(key, `onContextmenu`)
}
if (nonKeyModifiers.includes('middle')) {
key = transformClick(key, `onMouseup`)
}
if (nonKeyModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp,
JSON.stringify(nonKeyModifiers)
])
}
if (
keyModifiers.length &&
// if event name is dynamic, always wrap with keys guard
(!isStaticExp(key) || isKeyboardEvent(key.content))
) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
handlerExp,
JSON.stringify(keyModifiers)
])
}
if (eventOptionModifiers.length) {
const modifierPostfix = eventOptionModifiers.map(capitalize).join('')
key = isStaticExp(key)
? createSimpleExpression(`${key.content}${modifierPostfix}`, true)
: createCompoundExpression([`(`, key, `) + "${modifierPostfix}"`])
}
return {
props: [createObjectProperty(key, handlerExp)]
}
})
}dir: 就是directive,就是我们的@click=“xxx”node:自然就是这个dir所在节点,比如<button>...</button>modifiers: 前面说过了,比如@click.stop="xxx",这个stop即是modifier,也就是修饰符baseResult,也就是我们刚分析完的baseTransform最后返回的compund节点。resolveModifiers: 看名字是用来处理修饰符的,我们来看下代码
resolveModifier#
先来看下v-on的修饰符

const resolveModifiers = (
key: ExpressionNode,
modifiers: string[],
context: TransformContext,
loc: SourceLocation
) => {
const keyModifiers = []
const nonKeyModifiers = []
const eventOptionModifiers = []
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i]
if (
__COMPAT__ &&
modifier === 'native' &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_ON_NATIVE,
context,
loc
)
) {
eventOptionModifiers.push(modifier)
} else if (isEventOptionModifier(modifier)) {
// eventOptionModifiers: modifiers for addEventListener() options,
// e.g. .passive & .capture
eventOptionModifiers.push(modifier)
} else {
// runtimeModifiers: modifiers that needs runtime guards
if (maybeKeyModifier(modifier)) {
if (isStaticExp(key)) {
if (isKeyboardEvent((key as SimpleExpressionNode).content)) {
keyModifiers.push(modifier)
} else {
nonKeyModifiers.push(modifier)
}
} else {
keyModifiers.push(modifier)
nonKeyModifiers.push(modifier)
}
} else {
if (isNonKeyModifier(modifier)) {
nonKeyModifiers.push(modifier)
} else {
keyModifiers.push(modifier)
}
}
}
}
return {
keyModifiers,
nonKeyModifiers,
eventOptionModifiers
}
}checkCompatEnabled: 判断是否是可兼容该修饰符的版本,native修饰符是准备废弃掉了[9][10]。代码就没必要看了。maybeKeyModifier:left,right这俩货checkCompatEnabled: 判断是否是可兼容该修饰符的版本。native修饰符是准备废弃掉了isEventOptionModifier:passive,once,capture这仨货maybeKeyModifier:left,right这俩货isKeyboardEvent:onkeyup,onkeydown,onkeypress这三个事件isNonKeyModifier:stop,prevent,self,ctrl,shift,alt,meta,exact,middle这些剩下的
这个方法简单的说就是在给modifier也就是修饰符分门别类。
然后回到我们的transformOn中
transformClick: 来看下代码
const transformClick = (key: ExpressionNode, event: string) => {
const isStaticClick =
isStaticExp(key) && key.content.toLowerCase() === 'onclick'
return isStaticClick
? createSimpleExpression(event, true)
: key.type !== NodeTypes.SIMPLE_EXPRESSION
? createCompoundExpression([
`(`,
key,
`) === "onClick" ? "${event}" : (`,
key,
`)`
])
: key
}这个方法挺好理解的,如果是写死的click,比如@click.middle/right,那么直接就替换为mouseup/onContextmenu,如果是动态的@[event].left/right,那就得判断,如果命中click事件就替换。
isKeyboardEvent: 是否是键盘点击事件onkeyup,onkeydown,onkeypress这三货
总结下@vue/compiler-dom里的vOn.ts做了什么
其实做的事情很简单,处理modifier,表达式和事件名已经被@vue/compiler-core里的vOn.ts处理完了。
先是判断native这个修饰符的兼容性,如果可用就依旧保留,放在eventOptionModifier也就是事件选项修饰符队列中。
然后整理整理下v-on修饰符的四种类型
- 事件选项修饰符,这种是通用的:
passive,once,capture,native(需要考虑兼容性) - 可能是键入事件修饰符:
left,right,可以被鼠标/键盘使用(需要根据具体事件类型:onkeyup,onkeydown,onkeypress) - 非键入事件修饰符:
stop,prevent,self,ctrl,shift,alt,meta,exact,middle - 其余上面未识别的,也就是说是
.{keyAlias}
最后一种算在键入事件修饰符队列中,也就是keyModifiers。
接着重写使用.right和.middle修饰符的事件,如果是事件名是静态的并且是click事件,直接将事件名替换为onContextmenu/mouseup,如果是静态非click,保持原来的事件名,而如果是动态的事件名,这个时候就得创建compoundExpression节点,runtime的时候再去判断事件类型,如果到时判断是click,那就替换,否则不做处理。
然后封装事件名对应的value,也就是表达式节点,比如@click="xxx"的xxx被core包的vOn转换后的节点。
- 如果是非键入事件修饰符,将表达式节点和修饰符用
vOnModifiersGuard这个runtime的辅助函数包裹起来 - 如果是键入事件修饰符,那么判断是否是动态事件名或者是
onkeyup,onkeydown,onkeypress这仨货,如果是,则将表达式节点和修饰符用vOnKeysGuard这个runtime helper包裹起来。 - 如果是非键入事件修饰符,将修饰符名首字母大写之后拼接到事件名后面。
最后重新拼接为props。

v-show#
export const transformShow: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (!exp) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc)
)
}
return {
props: [],
needRuntime: context.helper(V_SHOW)
}
}V_SHOW:
export const V_SHOW = Symbol(__DEV__ ? `vShow` : ``)也是有点没有没脑的,先mark起来。
其它#
v-model的baseTransform#
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir
if (!exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc)
)
return createTransformProps()
}
const rawExp = exp.loc.source
const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp
// im SFC <script setup> inline mode, the exp may have been transformed into
// _unref(exp)
const bindingType = context.bindingMetadata[rawExp]
const maybeRef =
!__BROWSER__ &&
context.inline &&
bindingType &&
bindingType !== BindingTypes.SETUP_CONST
if (
!expString.trim() ||
(!isMemberExpression(expString, context) && !maybeRef)
) {
context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
)
return createTransformProps()
}
if (
!__BROWSER__ &&
context.prefixIdentifiers &&
isSimpleIdentifier(expString) &&
context.identifiers[expString]
) {
context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE, exp.loc)
)
return createTransformProps()
}
const propName = arg ? arg : createSimpleExpression('modelValue', true)
const eventName = arg
? isStaticExp(arg)
? `onUpdate:${arg.content}`
: createCompoundExpression(['"onUpdate:" + ', arg])
: `onUpdate:modelValue`
let assignmentExp: ExpressionNode
const eventArg = context.isTS ? `($event: any)` : `$event`
if (maybeRef) {
if (bindingType === BindingTypes.SETUP_REF) {
// v-model used on known ref.
assignmentExp = createCompoundExpression([
`${eventArg} => ((`,
createSimpleExpression(rawExp, false, exp.loc),
`).value = $event)`
])
} else {
// v-model used on a potentially ref binding in <script setup> inline mode.
// the assignment needs to check whether the binding is actually a ref.
const altAssignment =
bindingType === BindingTypes.SETUP_LET ? `${rawExp} = $event` : `null`
assignmentExp = createCompoundExpression([
`${eventArg} => (${context.helperString(IS_REF)}(${rawExp}) ? (`,
createSimpleExpression(rawExp, false, exp.loc),
`).value = $event : ${altAssignment})`
])
}
} else {
assignmentExp = createCompoundExpression([
`${eventArg} => ((`,
exp,
`) = $event)`
])
}
const props = [
// modelValue: foo
createObjectProperty(propName, dir.exp!),
// "onUpdate:modelValue": $event => (foo = $event)
createObjectProperty(eventName, assignmentExp)
]
// cache v-model handler if applicable (when it doesn't refer any scope vars)
if (
!__BROWSER__ &&
context.prefixIdentifiers &&
!context.inVOnce &&
context.cacheHandlers &&
!hasScopeRef(exp, context.identifiers)
) {
props[1].value = context.cache(props[1].value)
}
// modelModifiers: { foo: true, "bar-baz": true }
if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) {
const modifiers = dir.modifiers
.map(m => (isSimpleIdentifier(m) ? m : JSON.stringify(m)) + `: true`)
.join(`, `)
const modifiersKey = arg
? isStaticExp(arg)
? `${arg.content}Modifiers`
: createCompoundExpression([arg, ' + "Modifiers"'])
: `modelModifiers`
props.push(
createObjectProperty(
modifiersKey,
createSimpleExpression(
`{ ${modifiers} }`,
false,
dir.loc,
ConstantTypes.CAN_HOIST
)
)
)
}
return createTransformProps(props)
}
function createTransformProps(props: Property[] = []) {
return { props }
}代码有点小长
prefixIdentifiers: 暂时还不知道是干啥用的。isSimpleIdentifier: 一个正则,什么意思呢?如果这个v-model传递的变量如果没有.这玩意儿,就意味着这变量不是来自ctx的,那么就是template local变量,这种是不允许的,看下面的一点。 或者是一个函数调用表达式,比如a(),也是不允许的。
const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)X_V_MODEL_ON_SCOPE_VARIABLE: 来看下错误文案:v-model cannot be used on v-for or v-slot scope variables because they are not writable.也就是v-model传入的表达式不允许是v-for或者v-slot里的变量,因为它们不可写(not writable)。也就是上面说的template local变量。rawExp: 原始的表达式,存储的就是你代码写的,比如v-model="xx",这个xx就是原始的表达式,而转换后会变成_ctx.xx也就是下面的exp.content。exp.content:也就是上面说的_ctx.xx。bindingMetadata: 我记得在说compileScript的时候有说过,用来绑定数据的类型,但是这是编译状态,怎么会有来自script的bindingMetadata呢?其实还有inline template也就是inline mode,只不过我们并没有分析

isStaticExp:
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStaticpropName: 也就是传入子组件或者那几个标签的props属性。eventName:自然就是onUpdate:xxx。
你应该注意到了这个arg了,这个arg自然和上面提到过的那个arg是同一个,而这里居然有static之分?实际上这个是vue3.x提供的语法[12]

两种写法都是可以的,区别在于bb会去找对应的数据再转换为字符串也就是dynamic,而dynamic自然就只能是runtime的时候才能确定了。
注意,这仅能用于自定义组件使用,其它比如input、textarea都是不能用的。
来看下数据


当然,如果没有,默认使用modelValue这个名字。
maybeRef: 看名字就知道是判断是否是ref变量,之前我们分析script的时候有说过如何判断这个变量的类型的,当然,并不一定准确,有些变量可能在runtime的时候变成了ref或者unref。

你应该看到了else语句也有一个表达式生成,这也就是说你定义在setup里的unref变量也是可以通过v-model进入到自组件的。毕竟变量定义在setup block中最终是会被return 出来的,不管是不是ref,我们在分析script的时候说过了,这里就不多说了。
createObjectProperty:
export function createObjectProperty(
key: Property['key'] | string,
value: Property['value']
): Property {
return {
type: NodeTypes.JS_PROPERTY,
loc: locStub,
key: isString(key) ? createSimpleExpression(key, true) : key,
value
}
} 生成一个type == JS_PROPERTY的属性节点
hasScopeRef: 代码我们就不看了,简单的说就是判断这个节点以及它里面的所有节点是否有引用/使用到template local变量(仅当前环境,该方法较通用,具体参数具体分析)。context.cache(props[1].value): 给数据节点做一层cache处理,这里暂时不知道怎么处理的,所以先mark下来。modifiers[13]: 看名字就知道是修饰符,比如.lazy之类的,在组件中是以props的方式传入一个对象默认名字是modelModifier,实际会根据你的arg名字来生成,规则是${arg.content}Modifiers,这个prop默认是一个空对象。
简单的总结下这个方法
- 限制使用
template local variable也就是非_ctx.xxx,而是v-for等场景自带的variable。 arg也就是绑定的变量名,允许动态传入(动态的无法确定,所以只能是runtime的时候再去处理)- 修饰符,会以
${arg.content}Modifiers为名的prop传入,默认是一个空对象。 - 返回一个
{ props },这个是props默认存在三个字段modelValue、update:moduleValue以及modelModifier
漏了一个点,那就是这个update:modelValue传入的value是一个回调,而 $emit('update:modelvalue')实际上只是去执行这个回调。
processExpression#
export function processExpression(
node: SimpleExpressionNode,
context: TransformContext,
// some expressions like v-slot props & v-for aliases should be parsed as
// function params
asParams = false,
// v-on handler values may contain multiple statements
asRawStatements = false,
localVars: Record<string, number> = Object.create(context.identifiers)
): ExpressionNode {
if (__BROWSER__) {
if (__DEV__) {
// simple in-browser validation (same logic in 2.x)
validateBrowserExpression(node, context, asParams, asRawStatements)
}
return node
}
if (!context.prefixIdentifiers || !node.content.trim()) {
return node
}
const { inline, bindingMetadata } = context
//...省略`rewriteIdentifier`,等下会说到,所以代码切出来,这里就不重复展示了
// fast path if expression is a simple identifier.
const rawExp = node.content
// bail constant on parens (function invocation) and dot (member access)
const bailConstant = rawExp.indexOf(`(`) > -1 || rawExp.indexOf('.') > 0
if (isSimpleIdentifier(rawExp)) {
const isScopeVarReference = context.identifiers[rawExp]
const isAllowedGlobal = isGloballyWhitelisted(rawExp)
const isLiteral = isLiteralWhitelisted(rawExp)
if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) {
// const bindings exposed from setup can be skipped for patching but
// cannot be hoisted to module scope
if (bindingMetadata[node.content] === BindingTypes.SETUP_CONST) {
node.constType = ConstantTypes.CAN_SKIP_PATCH
}
node.content = rewriteIdentifier(rawExp)
} else if (!isScopeVarReference) {
if (isLiteral) {
node.constType = ConstantTypes.CAN_STRINGIFY
} else {
node.constType = ConstantTypes.CAN_HOIST
}
}
return node
}
let ast: any
// exp needs to be parsed differently:
// 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
// exp, but make sure to pad with spaces for consistent ranges
// 2. Expressions: wrap with parens (for e.g. object expressions)
// 3. Function arguments (v-for, v-slot): place in a function argument position
const source = asRawStatements
? ` ${rawExp} `
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parse(source, {
plugins: context.expressionPlugins
}).program
} catch (e: any) {
context.onError(
createCompilerError(
ErrorCodes.X_INVALID_EXPRESSION,
node.loc,
undefined,
e.message
)
)
return node
}
type QualifiedId = Identifier & PrefixMeta
const ids: QualifiedId[] = []
const parentStack: Node[] = []
const knownIds: Record<string, number> = Object.create(context.identifiers)
walkIdentifiers(
ast,
(node, parent, _, isReferenced, isLocal) => {
if (isStaticPropertyKey(node, parent!)) {
return
}
// v2 wrapped filter call
if (__COMPAT__ && node.name.startsWith('_filter_')) {
return
}
const needPrefix = isReferenced && canPrefix(node)
if (needPrefix && !isLocal) {
if (isStaticProperty(parent!) && parent.shorthand) {
// property shorthand like { foo }, we need to add the key since
// we rewrite the value
;(node as QualifiedId).prefix = `${node.name}: `
}
node.name = rewriteIdentifier(node.name, parent, node)
ids.push(node as QualifiedId)
} else {
// The identifier is considered constant unless it's pointing to a
// local scope variable (a v-for alias, or a v-slot prop)
if (!(needPrefix && isLocal) && !bailConstant) {
;(node as QualifiedId).isConstant = true
}
// also generate sub-expressions for other identifiers for better
// source map support. (except for property keys which are static)
ids.push(node as QualifiedId)
}
},
true, // invoke on ALL identifiers
parentStack,
knownIds
)
// We break up the compound expression into an array of strings and sub
// expressions (for identifiers that have been prefixed). In codegen, if
// an ExpressionNode has the `.children` property, it will be used instead of
// `.content`.
const children: CompoundExpressionNode['children'] = []
ids.sort((a, b) => a.start - b.start)
ids.forEach((id, i) => {
// range is offset by -1 due to the wrapping parens when parsed
const start = id.start - 1
const end = id.end - 1
const last = ids[i - 1]
const leadingText = rawExp.slice(last ? last.end - 1 : 0, start)
if (leadingText.length || id.prefix) {
children.push(leadingText + (id.prefix || ``))
}
const source = rawExp.slice(start, end)
children.push(
createSimpleExpression(
id.name,
false,
{
source,
start: advancePositionWithClone(node.loc.start, source, start),
end: advancePositionWithClone(node.loc.start, source, end)
},
id.isConstant ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
)
)
if (i === ids.length - 1 && end < rawExp.length) {
children.push(rawExp.slice(end))
}
})
let ret
if (children.length) {
ret = createCompoundExpression(children, node.loc)
} else {
ret = node
ret.constType = bailConstant
? ConstantTypes.NOT_CONSTANT
: ConstantTypes.CAN_STRINGIFY
}
ret.identifiers = Object.keys(knownIds)
return ret
}代码有些长
rawExp: 原来的表达式isSimpleIdentifier: 前面讲过,判断你是否是template local variableisGloballyWhitelisted: 直接看下面这图

isLiteralWhitelisted: 字面量白名单。
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') BindingTypes.SETUP_CONST: 这个前面讲过挺多次的,首先他得是在setup block或者选项式里的setup属性里,然后得是非ref/reactive的const变量。不过在编译过后它也是会被setup抛出的,这点之前分析compileScript就说过了,所以自然也能用在template里。ConstantTypes: 是一个枚举,好像前面讲过来着。。等级越高越“静”。
/**
* Static types have several levels.
* Higher levels implies lower levels. e.g. a node that can be stringified
* can always be hoisted and skipped for patch.
*/
export const enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
CAN_STRINGIFY
} CAN_SKIP_PATCH:意思是打补丁(diff新旧vnode发生在这个时候)的时候应该不用管他了,可以绕过。rewriteIdentifier: 来看下这个方法
const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (inline) {
// x = y
const isAssignmentLVal =
parent && parent.type === 'AssignmentExpression' && parent.left === id
// x++
const isUpdateArg =
parent && parent.type === 'UpdateExpression' && parent.argument === id
// ({ x } = y)
const isDestructureAssignment =
parent && isInDestructureAssignment(parent, parentStack)
if (
type === BindingTypes.SETUP_CONST ||
type === BindingTypes.SETUP_REACTIVE_CONST ||
localVars[raw]
) {
return raw
} else if (type === BindingTypes.SETUP_REF) {
return `${raw}.value`
} else if (type === BindingTypes.SETUP_MAYBE_REF) {
// const binding that may or may not be ref
// if it's not a ref, then assignments don't make sense -
// so we ignore the non-ref assignment case and generate code
// that assumes the value to be a ref for more efficiency
return isAssignmentLVal || isUpdateArg || isDestructureAssignment
? `${raw}.value`
: `${context.helperString(UNREF)}(${raw})`
} else if (type === BindingTypes.SETUP_LET) {
if (isAssignmentLVal) {
// let binding.
// this is a bit more tricky as we need to cover the case where
// let is a local non-ref value, and we need to replicate the
// right hand side value.
// x = y --> isRef(x) ? x.value = y : x = y
const { right: rVal, operator } = parent as AssignmentExpression
const rExp = rawExp.slice(rVal.start! - 1, rVal.end! - 1)
const rExpString = stringifyExpression(
processExpression(
createSimpleExpression(rExp, false),
context,
false,
false,
knownIds
)
)
return `${context.helperString(IS_REF)}(${raw})${
context.isTS ? ` //@ts-ignore\n` : ``
} ? ${raw}.value ${operator} ${rExpString} : ${raw}`
} else if (isUpdateArg) {
// make id replace parent in the code range so the raw update operator
// is removed
id!.start = parent!.start
id!.end = parent!.end
const { prefix: isPrefix, operator } = parent as UpdateExpression
const prefix = isPrefix ? operator : ``
const postfix = isPrefix ? `` : operator
// let binding.
// x++ --> isRef(a) ? a.value++ : a++
return `${context.helperString(IS_REF)}(${raw})${
context.isTS ? ` //@ts-ignore\n` : ``
} ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
} else if (isDestructureAssignment) {
// TODO
// let binding in a destructure assignment - it's very tricky to
// handle both possible cases here without altering the original
// structure of the code, so we just assume it's not a ref here
// for now
return raw
} else {
return `${context.helperString(UNREF)}(${raw})`
}
} else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return genPropsAccessExp(raw)
} else if (type === BindingTypes.PROPS_ALIASED) {
// prop with a different local alias (from defineProps() destructure)
return genPropsAccessExp(bindingMetadata.__propsAliases![raw])
}
} else {
if (type && type.startsWith('setup')) {
// setup bindings in non-inline mode
return `$setup.${raw}`
} else if (type === BindingTypes.PROPS_ALIASED) {
return `$props['${bindingMetadata.__propsAliases![raw]}']`
} else if (type) {
return `$${type}.${raw}`
}
}
// fallback to ctx
return `_ctx.${raw}`
}hasOwn: 这个没啥好看的,就是判断这个对象是否包含这个属性bindingMetadata:还记得之前说的吗?我们并没有分析inlineTemplate的场景,而inlineTemplate场景是在compileScript的时候就进行的。而这个bindingMetadata就是之前compileScript调用@vue/compiler-dom的compile时传入的。它是一个集合,包含着setup这一块里面所有变量以及他们的类型,key是变量名,而value则是这个变量在setup中是什么类型,比如setup-ref或者setup-may-ref等。isAssignmentLVal: 赋值表达式左边,比如x = y的xisUpdateArg: 自加表达式, 比如x++isDestructureAssignment: 解构表达式,比如({ x } = y)BindingTypes: 之前说过,它是一个枚举,里面存放的是所有setup block可能出现的变量类型。
export const 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'
}UNREF: 前面说过了,unref判断你是否是ref变量,是的话返回值,不是则返回这个变量。
export const UNREF = Symbol(__DEV__ ? `unref` : ``)
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}注意这是个runtime函数
IS_REF: 这个就更不多说了
export const IS_REF = Symbol(__DEV__ ? `isRef` : ``)
// 下面两个是重载,按顺序往下看哪个参数类型对上了就用哪个
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}genPropsAccessExp: 看下代码
const identRE = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/
export function genPropsAccessExp(name: string) {
return identRE.test(name)
? `__props.${name}`
: `__props[${JSON.stringify(name)}]`
}就是获取你这个defineProps变量的属性。
BindingTypes.PROPS_ALIASED: 还记得我们分析过的defineProps解构赋值的场景吗?比如const { a: _a } = defineProps({...});。__propsAliases: 上面这个_a可能会被用在template中,所以这里需要获取对应的原变量名。
总结一下这个rewriteIdentifier方法做了什么
一、inlineTemplate场景,场景就比较多和复杂
- 如果是普通本地变量/
setup块里的非ref的const变量/使用reactive包裹之后的变量,这些都不用处理的,该怎么样就怎么样。 - 如果是
setup ref的变量,那就改下返回的数据为.value,毕竟ref数据都在.value里。 SETUP_MAYBE_REF,也就是这个变量在编译的时候并不能知道它是个啥,场景其实挺多的,比如:const { a } = defineProps({...}); const b = a;。这个时候编译器拿到a压根就不知道是啥, 而a是通过props传入的一个ref对象。但是编译阶段是做不到跨组件的,说到底就是一个个sfc文件的编译转换。所以这个时候如果你的表达式是一个会改变变量对应的值或者被解构,如果不给它加上.value就会有问题,所以以下这三个场景需要特殊处理:1. 赋值表达式; 2. 自加/自减; 3. 解构场景。这个将值变成x.value,这样才不会有问题。当然这里你应该会有些奇怪,为什么这三个场景不用判断是否是ref就直接返回.value了。因为你这如果是一个普通变量那么这些表达式改变了值又有什么用呢,并不会应用于变量本身。然后除此之外的其它场景就直接判断是否是ref再返回值/变量了。BindingTypes.SETUP_LET: 这个用尤大大的原话来说就是:tricky,太狡猾了,为什么呢?因为它们可以在runtime的某一时刻变成ref又或者non-ref。编译器针对上面的三个场景分别做了以下处理:
-
赋值表达式,判断被赋值的对象也就是等号左边是否是
ref,是得话帮它加上.value,但是等号右边可能又是一个ref,所以这个时候就得递归把右边也放进去拿到对应的值才行。最后的表达式会变成x = y --> isRef(x) ? x.value = y(.value) : x = y(.value)。 -
自加/减,这个比较简单,只需要判断自加对象是否是
ref然后加上.value即可。不过由于自加自减有两种情况,++a或者a++,所以这个时候要先判断是++a还是a++才行。最后的表达式会变成a++ -> isRef(a) ? ++a.value(++) : ++a(++)。 -
解构,这个就更夸张了,由于场景太多,大大又补了一句
very stricky。目前没处理,仅是把它当作non ref,然后返回当前的变量。 -
其它场景直接
unref处理。
BindingTypes.PROPS:genPropsAccessExp上面说过了,返回_props.xxx或者_props[xxx]的表达式。BindingTypes.PROPS_ALIASED: 同上,不过得先找到对应的真名。
二、 传统non-inline
-
- 如果是一般
setup变量,比如const a = 1或者let b = 1,都会直接从$setup这个上下文中获取 - 如果是
props通过解构的方式给的别名,这个时候就得从$props上下文中获取对应的真名。 - 其它各找各妈。
- 如果是一般
三、最后返回_ctx.${raw},兜底。
看到这里你应该知道这个方法仅能用于简单的表达式。
ok 这玩意儿终于讲完了,我们接着回到我们的processExpression中
parse: 自然就是@babel/parser[14]里的parse。asRawStatements: 这个是之前传入的变量const hasMultipleStatements = exp.content.includes(';'), 用;分隔判断是否是多个表达式。比如a++; b();前面有说到过了,这样是可以的。walkIdentifiers: 这个就不看代码了,前面分析compileScript的时候有说到过,简单的说下,它也是基于estree-walker[15]这个包来改变节点的位置,这个包之前有提到过。这里用它来遍历所有节点。__COMPAT__: 全局变量,用来判断兼容V2里的一些语法的,比如filter,在3.x就废弃了。isReferenced: 应该是指的这个expression是否是一个引用的,这里就不纠结来源了,里面涉及到的代码很多,后面如果遇到再分析了。isLocal: 是否是template local变量canPrefix:
function canPrefix(id: Identifier) {
// skip whitelisted globals
if (isGloballyWhitelisted(id.name)) {
return false
}
// special case for webpack compilation
if (id.name === 'require') {
return false
}
return true
}暂时不清楚这个canPrefix是做什么的,不过看名字可以推测是要加上前缀,而一些全局白名单对象以及 webpack特有的require引入都是false。
isStaticProperty: 前面说过好几次了,这里就不多说了.shorthand: 指的是简写,比如{ a: a }可以缩写成{ a }.QualifiedId: 这个给忘了,来看下
type QualifiedId = Identifier & PrefixMeta
interface Identifier extends BaseNode {
type: "Identifier";
name: string;
decorators?: Array<Decorator> | null;
optional?: boolean | null;
typeAnnotation?: TypeAnnotation | TSTypeAnnotation | Noop | null;
}
interface PrefixMeta {
prefix?: string
isConstant: boolean
start: number
end: number
scopeIds?: Set<string>
} 估计你应该忘了我们的node长啥样了 ,这里再看一次,因为后面需要和这个方法作用域里的node区分开来

ids:多表达式转换成数组

leadingText: 两个表达式变量之间的内容,比如

children: 说那么多不如一张图直接

createCompoundExpression: 这个之前说过很多次了,如果是编译过程中无法处理的问题,那就是动态dynamic的,这个时候就会创建一个compoundExpression交给runtime处理了。ret: 最后返回的数据

ok,让我们来总结下这个方法做了什么
根据当前表达式是否带有;分为下面两种情况
一、单表达式
- 如果不是参数 && 不是
template local variable&& 不是内置全局变量&&非字面量,这种时候就会调用rewriteIdentifier匹配重写这个表达式的节点。另外,如果是setup-const类型的节点,会被标记为CAN_SKIP_PATCH,也就是打补丁阶段可以直接跳过,毕竟是一个固定的变量。 - 如果不是上面的场景,判断是否是
template local variable也就是v-for或者v-slot产生的变量。这种场景下,判断是否是字面量,如果是则可以直接CAN_STRINGIFY,也就是以后都可以直接不理会。如果不是字面量,那就有可能是全局的变量,比如JSON等,这种被标记为CAN_HOIST,也就是提升。这个CAN_STRINGIFY是最稳定的,CAN_HOIST次之。
单表达式又存在两种场景
inlinenon-inline
至于如何分辨、处理,请往回看rewriteIdentifier的总结。
标记最终的节点constType类型,如果是带有.或者()的单表达式,那么标记为NOT_CONSTANT,否则是CAN_STRINGIFY 。也就是说,要么是可变的,要么是不可变的。
返回最终的节点。
二、多表达式
- 调用
@babel/parser的parser方法将表达式转换成ast - 然后调用
estree-walker遍历ast,收集所有表达式里的变量也就是indentifier,如果是常规变量则会调用rewriteIdetifier处理,否则除了v-for/v-slot中产生的变量之外(比如字面量),其它都标记为isConstant。 - 按代码位置顺序(
loc.start/end)排序收集到的变量,然后顺便截取这些个变量之间的内容组成一个children的数组 - 最后将
children转换为一个compoundExpression节点,然后返回。
简单的说就是根据不同场景处理表达式,转换成一个节点。
总结#
今天我们分析了几个指令是如何处理的,其中v-model以及v-on的比较麻烦,毕竟场景较多。而v-cloak/v--show有点摸不着头脑,需要mark下来,后面应该还会遇到。
如果觉得这篇文章对你有帮助,请务必点个赞,谢谢~
参考#
- ^v-cloack https://vuejs.org/api/built-in-directives.html#v-cloak
- ^v-html https://vuejs.org/api/built-in-directives.html#v-html
- ^v-text https://vuejs.org/api/built-in-directives.html#v-text
- ^v-model https://vuejs.org/api/built-in-directives.html#v-model
- ^useage_with_v-model https://vuejs.org/guide/components/events.html#usage-with-v-model
- ^vue3.x_v-on https://vuejs.org/api/built-in-directives.html#v-on
- ^@vue/compiler-core https://github.com/vuejs/core/tree/main/packages/compiler-core
- ^babel-memberExpression https://babeljs.io/docs/en/babel-types#memberexpression
- ^vue3.x-deprecate-native-modifier https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html
- ^vue3.x-new-emits-option https://v3-migration.vuejs.org/breaking-changes/emits-option.html#emits-option
- ^vue-v-show https://vuejs.org/api/built-in-directives.html#v-show
- ^vue3.x-multiple-v-model-in-components https://vuejs.org/guide/components/events.html#usage-with-v-model
- ^vue3.x-use-modifier-with-v-model-in-components https://vuejs.org/guide/components/events.html#handling-v-model-modifiers
- ^@babel/parser https://babeljs.io/docs/en/babel-parser
- ^estree-walker https://www.npmjs.com/package/estree-walker
发布于 2022-12-21 11:21・IP 属地广东
