闽公网安备 35020302035485号
<MyComponent :disabled="!valid" :data="someTestData" @confirm="handleConfirm" />它定义了 data 和 disabled 作为 props,前者作为组件的数据输入,后者用来定义组件的功能开关。组件被点击时,会抛出 confirm 事件,不过当 disabled 为 true 时,confirm 事件不会被触发。
describe('MyComponent on the page', () => {
// ...
it('confirm event', async () => {
const instance = wrapper.findComponent({ name: 'MyComponent' })
const spy = vi
.spyOn(wrapper.vm, 'handleConfirm')
.mockImplementation(() => null)
await instance.trigger('click')
expect(spy).not.toHaveBeenCalled()
// ... change valid
await instance.trigger('click')
expect(spy).toHaveBeenCalledTimes(1)
})
})
valid 初始化时为 false,即 MyComponent 一开始不会抛出 confirm 事件,当valid 被改变后,点击 MyComponent,confirm 事件才被抛出。这段单元测试会在最后一句报错,显示 spy 实际被触发 0 次。实际上,spy 永远不会被触发,即使 valid 初始化时为 true 也是如此。然而,将模板里的方法调用调整一下,加上括号,单元测试就按照预期通过了:<MyComponent :disabled="!valid" :data="someTestData" @confirm="handleConfirm()" />为什么加不加括号会引起单元测试的逻辑变化?
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// ...
// 1. 生成基础ast
const ast = isString(template) ? baseParse(template, options) : template
// ...
// 2. 对ast做转换
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 3.生成渲染函数
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
1、调用 baseParse 方法解析 HTML,生成基础的 AST。由于 Vue 在 HTML 上增加了许多语法特性(v-if、v-for、v-bind 等等),需要做对应解析。<div @click="handleConfirm()" /> 生成的 AST
<div @click="handleConfirm" /> 生成的 AST

export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
光从名字就可以看出来,依旧是对 Vue 的语法特性做的一些工作,最终在 AST 的每个节点上增加 codegenNode,这个属性将会被用在第三步生成渲染函数过程中。经过 transform 这一步后,生成的 codegenNode 如下:<div @click="handleConfirm()" /> 的 codegenNode
<div @click="handleConfirm" /> 的 codegenNode

const isMemberExp = isMemberExpression(exp.content, context)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement || (shouldCache && isMemberExp)) {
// 堆代码 duidaima.com
// 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 ? `}` : `)`,
]);
}
首先对 exp 做判断,是否是 member expression、是否是 inline statement,是否有多个 statement。然后出现了 exp 的改写,根据判断生成了 compound expression,实际就是转换成了函数表达。看来 isMemberExp、isInlineStatement 这两个判断影响了最终 codegenNode 的生成。const a = { x: 0 }const b = a.x
这里 a.x 就是 member expression,transformOn 中调用 isMemberExpression 来做判断,实际就是调用 babel parser 的能力分析,简化来说:try {
let ret: Expression = parseExpression(path, {
plugins: context.expressionPlugins,
});
if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
ret = ret.expression;
}
return (
ret.type === 'MemberExpression' ||
ret.type === 'OptionalMemberExpression' ||
ret.type === 'Identifier'
);
} catch (e) {
return false;
}
这里 MemberExpression、OptionalMemberExpression、Identifier 都被认定成了 member expression。OptionalMemberExpression 即带有 optional chaining (?.) 的表达式。Identifier 也被包括的原因是,在模板中一般会省略主对象,如 this、或者 setup 中返回的对象。
<MyComponent :disabled="!valid" :data="someTestData" @confirm="hasConfirmed = $event" />而让这段代码生效的原因,就在于 transformOn 编译时将 exp 包裹了一层函数声明。它调用 createCompoundExpression,将 $event 作为函数入参,从而使函数内能获取到:
($event) => (hasConfirmed = $event)3、由上一步生成的 codegenNode,转换成最终的渲染函数。重点看一下带括号的表达式生成的渲染函数:
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", {
onClick: $event => (handleConfirm())
}, null, 8 /* PROPS */, ["onClick"]))
}
}
with statement 是在模板中可以省略 this 的原因。const ctx = { handleConfirm: () => null }const prop = { onClick: ($event) => { ctx.handleConfirm() } }
不带括号的函数表达:const ctx = { handleConfirm: () => null }const prop = { onClick: ctx.handleConfirm }
function spyOn(obj, method) {
let spy = {
args: [],
};
let original = obj[method];
obj[method] = function () {
let args = [].slice.apply(arguments);
spy.count++;
spy.args.push(args);
return original.call(obj, args);
};
return Object.freeze(spy);
}
它将 object[method] 指向了新的函数,首先更新函数执行的次数、记录每次执行的入参,然后用 call 执行原始函数。