支持 v-on 指令
重构
在进行实现之前,我们先进行一些重构。
目前,在codegen生成的代码中,我们从shared和runtime-core导入(或通过解构赋值读取)了许多helper函数。
而在codegen(和transform)的实现中,我们直接硬编码了这些函数名,这种做法不太优雅。
这次,我们将把这些函数作为runtime-helper统一通过symbol管理,并且只导入我们需要的部分。
首先,我们在compiler-core/runtimeHelpers.ts
中实现表示各个helper的symbol。
之前我们一直使用h函数来生成VNode,这次我们参考Vue官方实现,改为使用createVNode。
从runtime-core/vnode导出createVNode,并修改genVNodeCall以调用createVNode代码。
export const CREATE_VNODE = Symbol()
export const MERGE_PROPS = Symbol()
export const NORMALIZE_CLASS = Symbol()
export const NORMALIZE_STYLE = Symbol()
export const NORMALIZE_PROPS = Symbol()
export const helperNameMap: Record<symbol, string> = {
[CREATE_VNODE]: 'createVNode',
[MERGE_PROPS]: 'mergeProps',
[NORMALIZE_CLASS]: 'normalizeClass',
[NORMALIZE_STYLE]: 'normalizeStyle',
[NORMALIZE_PROPS]: 'normalizeProps',
}
我们需要支持在CallExpression的callee中使用symbol:
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
callee: string | symbol
}
接下来在TransformContext中添加注册helper的功能:
export interface TransformContext extends Required<TransformOptions> {
currentNode: RootNode | TemplateChildNode | null
parent: ParentNode | null
childIndex: number
helpers: Map<symbol, number> // 新增
helper<T extends symbol>(name: T): T // 新增
}
export function createTransformContext(
root: RootNode,
{ nodeTransforms = [], directiveTransforms = {} }: TransformOptions,
): TransformContext {
const context: TransformContext = {
// .
// .
// .
helpers: new Map(),
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
}
return context
}
然后,我们将硬编码的部分替换为这个helper函数,并修改Preamble以使用注册的helper:
// 例)
propsExpression = createCallExpression('mergeProps', mergeArgs, elementLoc)
// ↓
propsExpression = createCallExpression(
context.helper(MERGE_PROPS),
mergeArgs,
elementLoc,
)
我们还需要在createVNodeCall中传入context,并在其中注册CREATE_VNODE:
export function createVNodeCall(
context: TransformContext | null, // 新增
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
loc: SourceLocation = locStub,
): VNodeCall {
// 新增部分 ------------------------
if (context) {
context.helper(CREATE_VNODE)
}
// ------------------------
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
loc,
}
}
function genVNodeCall(
node: VNodeCall,
context: CodegenContext,
option: Required<CompilerOptions>,
) {
const { push, helper } = context
const { tag, props, children } = node
push(helper(CREATE_VNODE) + `(`, node) // 改为调用createVNode
genNodeList(genNullableArgs([tag, props, children]), context, option)
push(`)`)
}
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
root.helpers = new Set([...context.helpers.keys()]) // 在root中保存helpers
}
// 参考官方实现,使用`_`作为前缀
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const { push, newline, runtimeGlobalName } = context
// 根据ast中注册的helper生成helper声明
const helpers = Array.from(ast.helpers)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = ${runtimeGlobalName}\n`,
)
newline()
}
// 在genCallExpression中处理symbol并转换为helper调用
export interface CodegenContext {
// .
// .
// .
helper(key: symbol): string
}
function createCodegenContext(ast: RootNode): CodegenContext {
const context: CodegenContext = {
// .
// .
// .
helper(key) {
return `_${helperNameMap[key]}`
},
}
// .
// .
// .
return context
}
// .
// .
// .
function genCallExpression(
node: CallExpression,
context: CodegenContext,
option: Required<CompilerOptions>,
) {
const { push, helper } = context
// 如果是symbol,从helper获取
const callee = isString(node.callee) ? node.callee : helper(node.callee)
push(callee + `(`, node)
genNodeList(node.arguments, context, option)
push(`)`)
}
这样我们的重构就完成了,之前硬编码的部分变得更加整洁了!
编译结果
※ 注意
- input使用的是前一章playground的内容
- 实际上
function
前面有return
- 生成的代码通过prettier进行了格式化
看起来还有一些多余的换行和空白,不是很美观...
我们以后再进行优化吧。
function render(_ctx) {
with (_ctx) {
const {
normalizeProps: _normalizeProps,
createVNode: _createVNode,
normalizeClass: _normalizeClass,
} = ChibiVue
return _createVNode('div', null, [
'\n ',
_createVNode('p', _normalizeProps({ id: count }), ' v-bind:id="count" '),
'\n ',
_createVNode(
'p',
_normalizeProps({ id: count * 2 }),
' :id="count * 2" ',
),
'\n\n ',
_createVNode(
'p',
_normalizeProps({ ['style' || '']: bind.style }),
' v-bind:["style"]="bind.style" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({ ['style' || '']: bind.style }),
' :["style"]="bind.style" ',
),
'\n\n ',
_createVNode('p', _normalizeProps(bind), ' v-bind="bind" '),
'\n\n ',
_createVNode(
'p',
_normalizeProps({ style: { 'font-weight': 'bold' } }),
' :style="{ font-weight: \'bold\' }" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({ style: 'font-weight: bold;' }),
' :style="\'font-weight: bold;\'" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({
class: _normalizeClass('my-class my-class2'),
}),
' :class="\'my-class my-class2\'" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({ class: _normalizeClass(['my-class']) }),
' :class="[\'my-class\']" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({
class: _normalizeClass({ 'my-class': true }),
}),
' :class="{ \'my-class\': true }" ',
),
'\n ',
_createVNode(
'p',
_normalizeProps({
class: _normalizeClass({ 'my-class': false }),
}),
' :class="{ \'my-class\': false }" ',
),
'\n',
])
}
}
v-on
本次目标的开发者接口
现在让我们开始实现v-on指令。
v-on同样拥有多种开发者接口。
https://vuejs.org/guide/essentials/event-handling.html
我们这次的目标大致如下:
import { createApp, defineComponent, ref } from 'chibivue'
const App = defineComponent({
setup() {
const count = ref(0)
const increment = (e: Event) => {
console.log(e)
count.value++
}
return { count, increment, state: { increment }, eventName: 'click' }
},
template: `<div>
<p>count: {{ count }}</p>
<button v-on:click="increment">v-on:click="increment"</button>
<button v-on:[eventName]="increment">v-on:click="increment"</button>
<button @click="increment">@click="increment"</button>
<button v-on="{ click: increment }">v-on="{ click: increment }"</button>
<button @click="state.increment">v-on:click="increment"</button>
<button @click="count++">@click="count++"</button>
<button @click="() => count++">@click="() => count++"</button>
<button @click="increment($event)">@click="increment($event)"</button>
<button @click="e => increment(e)">@click="e => increment(e)"</button>
</div>`,
})
const app = createApp(App)
app.mount('#app')
需要实现的功能
实际上,Parser的实现在上一章已经足够,主要问题在于Transformer的实现。
主要是根据arg是否存在以及exp的形式种类来改变转换内容。 对于没有arg的情况,处理方式基本与v-bind相同。
因此,我们需要考虑的是arg存在时exp可能的形式种类,以及对它们进行必要的AST Node转换。
问题1
可以分配函数
这是最简单的情况。html<button v-on:click="increment">increment</button>
问题2
可以直接编写函数表达式 在这种情况下,可以接收事件作为第一个参数。html<button v-on:click="(e) => increment(e)">increment</button>
问题3
可以编写非函数语句html<button @click="count = 0">reset</button>
这个表达式需要转换为以下形式的函数:
ts;() => { count = 0 }
问题4
在问题3的情况下可以使用$event
标识符 这是处理事件对象的情况。tsconst App = defineComponent({ setup() { const count = ref(0) const increment = (e: Event) => { console.log(e) count.value++ } return { count, increment, object } }, template: ` <div class="container"> <button @click="increment($event)">increment($event)</button> <p> {{ count }} </p> </div> `, }) // 不能像 @click="() => increment($event)" 这样使用
需要转换为以下形式的函数:
ts$event => { increment($event) }
实现
没有arg的情况
首先,对于没有arg的情况,处理方式与v-bind相同,所以我们从这里开始实现。
这是前一章我们留下TODO注释的部分。在transformElement中:
const isVBind = name === 'bind'
const isVOn = name === 'on' // --------------- 这里
// special case for v-bind and v-on with no argument
if (!arg && (isVBind || isVOn)) {
if (exp) {
if (isVBind) {
pushMergeArg()
mergeArgs.push(exp)
} else {
// -------------------------------------- 这里
// v-on="obj" -> toHandlers(obj)
pushMergeArg({
type: NodeTypes.JS_CALL_EXPRESSION,
loc,
callee: context.helper(TO_HANDLERS),
arguments: [exp],
})
}
}
continue
}
const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) {
const { props } = directiveTransform(prop, node, context)
if (isVOn && arg && !isStaticExp(arg)) {
pushMergeArg(createObjectExpression(props, elementLoc))
} else {
properties.push(...props)
}
} else {
// TODO: custom directive.
}
我们需要实现一个名为TO_HANDLERS
的新helper函数。
这个函数将v-on="{ click: increment }"
形式的对象转换为{ onClick: increment }
形式。
实现起来并不复杂:
import { toHandlerKey } from '../../shared'
/**
* For prefixing keys in v-on="obj" with "on"
*/
export function toHandlers(obj: Record<string, any>): Record<string, any> {
const ret: Record<string, any> = {}
for (const key in obj) {
ret[toHandlerKey(key)] = obj[key]
}
return ret
}
这样就完成了没有arg的情况的实现。
现在来处理有arg的情况。
transformVOn
现在进入本章的主题。v-on的exp有各种形式:
increment
state.increment
count++
;() => count++
increment($event)
e => increment(e)
首先,这些形式可以大致分为两类:"函数"和"语句"。
在Vue中,单独的Identifier、单独的MemberExpression或函数表达式被视为函数。
其他的都是语句。在源代码中,这些被称为inlineStatement。
// function (※为了方便,这里会出现分号,但请将它们视为函数表达式)
increment
state.increment
;() => count++
// inlineStatement
count++
increment($event)
因此,我们的实现流程是:
- 首先判断是否为函数(单独的Identifier或单独的MemberExpression或函数表达式)
2-1. 如果是函数,则不进行任何转换,直接以eventName: exp
的形式生成ObjectProperty
2-2. 如果不是函数(即inlineStatement),则转换为$event => { ${exp} }
的形式,并生成ObjectProperty
判断是函数表达式还是语句
首先实现判断逻辑。 使用正则表达式来判断是否为函数表达式:
const fnExpRE =
/^\s*([\w$_]+|(async\s*)?\([^)]*?\))\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
const isFn = fnExpRE.test(exp.content)
然后,使用isMemberExpression
函数判断是否为单独的Identifier或单独的MemberExpression:
const isMemberExp = isMemberExpression(exp.content)
这个isMemberExpression
函数实现比较复杂,这里省略。(建议查看源代码了解详情。)
MemberExpression通常指parent.prop
这样的形式,但这个函数似乎也将根级别的标识符如ident
判断为true。
现在我们可以判断inlineStatement的条件,即不是以上两种情况:
const isMemberExp = isMemberExpression(exp.content)
const isFnExp = fnExpRE.test(exp.content)
const isInlineStatement = !(isMemberExp || isFnExp)
判断完成后,我们可以根据结果实现转换逻辑:
const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`,
])
}
问题点
上述实现实际上存在一个问题。
由于dir.exp
中处理的是setup中绑定的值,需要经过processExpression处理,但$event
是个特殊情况。
在AST中,$event
也被视为Identifier,这样会导致它被加上_ctx.
前缀。
为了解决这个问题,我们需要做一些调整。 在transformContext中添加注册本地变量的功能,并在walkIdentifiers中,如果存在本地变量,则不执行onIdentifier:
const context: TransformContext = {
// .
// .
// .
identifiers: Object.create(null),
// .
addIdentifiers(exp) {
if (!isBrowser) {
addId(exp)
}
},
removeIdentifiers(exp) {
if (!isBrowser) {
removeId(exp)
}
},
}
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
identifiers[id]!++
}
function removeId(id: string) {
context.identifiers[id]!--
}
export function walkIdentifiers(
root: Node,
onIdentifier: (node: Identifier) => void,
knownIds: Record<string, number> = Object.create(null),
) {
;(walk as any)(root, {
enter(node: Node) {
if (node.type === 'Identifier') {
const isLocal = !!knownIds[node.name]
// prettier-ignore
if (!isLocal) {
onIdentifier(node);
}
}
},
})
}
在processExpression中使用walkIdentifiers时,从context中获取identifiers:
const ids: QualifiedId[] = []
const knownIds: Record<string, number> = Object.create(ctx.identifiers)
walkIdentifiers(
ast,
node => {
node.name = rewriteIdentifier(node.name)
ids.push(node as QualifiedId)
},
knownIds,
)
最后,在transformOn中转换时注册$event
:
// prettier-ignore
if (!context.isBrowser) {
isInlineStatement && context.addIdentifiers(`$event`);
exp = dir.exp = processExpression(exp, context);
isInlineStatement && context.removeIdentifiers(`$event`);
}
if (isInlineStatement) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`,
])
}
由于v-on需要特殊处理,在transformOn中单独处理,因此需要在transformExpression中跳过它:
export const transformExpression: NodeTransform = (node, ctx) => {
// .
// .
// .
if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
) {
dir.exp = processExpression(exp, ctx)
}
}
至此,v-on指令的核心部分已经完成。我们只需实现剩余的必要部分就能完成v-on的实现!
完整的源代码:GitHub