Libon

从 Vue 事件修饰符到代码编译

12 分钟 #vue
从 .stop/.prevent... 事件修饰符再到代码编译生成,探究 Vue 事件修饰符的实现原理。

ToC

修饰符代表了什么

在 vue 开发中时常会看到 .stop .prevent .capture 等事件修饰符,这些修饰符是如何被实现的呢?首先我们知道,事件修饰符是在编译阶段被处理的,所以我们需要从编译阶段开始看起。以 Vue2 为例,我们很容易就可以找到这段代码,它在 src/compiler/codegen/events.ts. 修饰符的定义如下:

1
// #4868: modifiers that prevent the execution of the listener
2
// need to explicitly return null so that we can determine whether to remove
3
// the listener for .once
4
const genGuard = condition => `if(${condition})return null;`
5
// ↑ 传入的 condition 为判断条件
6
7
// 各种修饰符会生成的代码及它的判断条件
8
const modifierCode: { [key: string]: string } = {
9
// ↓ $event 参数是在使用了修饰符或者事件处理器是一个函数调用的话
12 collapsed lines
10
// vue 在编译的时候就会额外包装一层函数, 并提供 $event 形参参数
11
stop: '$event.stopPropagation();',
12
prevent: '$event.preventDefault();',
13
self: genGuard(`$event.target !== $event.currentTarget`),
14
ctrl: genGuard(`!$event.ctrlKey`),
15
shift: genGuard(`!$event.shiftKey`),
16
alt: genGuard(`!$event.altKey`),
17
meta: genGuard(`!$event.metaKey`),
18
left: genGuard(`'button' in $event && $event.button !== 0`),
19
middle: genGuard(`'button' in $event && $event.button !== 1`),
20
right: genGuard(`'button' in $event && $event.button !== 2`)
21
}

修饰符是如何生效的

可以看到,这里的修饰符除了 .stop.prevent 之外都会通过 genGuard 函数生成一个运行时的判断条件。比如 .stop.prevent 修饰符,这俩是直接插入 $event.stopPropagation();$event.stopPropagation(); 到最终生成的代码里,而其他的修饰符则会根据修饰符的进行变化。比如 .ctrl,生成的条件就是 if(!$event.ctrlKey), 其他的以此类推,如果条件为 true,则返回 return null,否则返回空字符串。为什么需要显式 return null ,也在注释中说明了原因:

modifiers that prevent the execution of the listener need to explicitly return null so that we can determine whether to remove the listener for .once
防止侦听器执行需要明确返回null的修饰符,以便我们确定是否删除了侦听器。

这里的 $event 是一个参数,它是在 src/compiler/codegen/events.ts 处,由 genHandler 方法生成的,它其实表示的是一个形参,会在编译的时候包装一个新的函数,然后传入到包装的函数内部。额外包装一个函数是为了确保这个事件处理句柄一定是一个函数,而不是类似 test(1,2,3) 这一类函数调用的表达式。

处理所有事件类型

genHandlers() 的代码比较简单,主要是处理了动态事件名和动态事件句柄,就不做详细解析了:

1
export function genHandlers(
2
events: ASTElementHandlers,
3
isNative: boolean
4
): string {
5
// 判断是否是原生事件
6
const prefix = isNative ? 'nativeOn:' : 'on:'
7
// 编译后的静态事件名的事件处理函数代码
8
let staticHandlers = ``
9
// 编译后的动态事件名的事件处理函数代码
25 collapsed lines
10
let dynamicHandlers = ``
11
// 遍历所有事件类型挨个生成事件处理函数
12
for (const name in events) {
13
const handlerCode = genHandler(events[name])
14
// 如果是动态事件名则编译为: test,function($event){...}, 并将事件名和事件处理函数名放入 dynamicHandlers 中
15
if (events[name] && events[name].dynamic) {
16
dynamicHandlers += `${name},${handlerCode},`
17
} else {
18
// 如果是静态事件名则编译为: "test":function($event){...},
19
staticHandlers += `"${name}":${handlerCode},`
20
}
21
}
22
// 删掉事件对象最后的逗号, 并增加 {}
23
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
24
25
// 如果有动态事件则会返回类似: nativeOn|on:_d({test,function($event){...}}) 的代码
26
if (dynamicHandlers) {
27
// _d 的实现先不看, 在后面会讲到这个函数最终表示的是什么
28
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
29
} else {
30
// 如果没有动态事件则直接返回静态事件的代码, 诸如:
31
// nativeOn|on:{"test":function($event){...}}
32
return prefix + staticHandlers
33
}
34
}

事件处理函数的生成

我们重点看一下 genHandler() 函数的实现:

1
// 判断是箭头函数还是 function 这种行内处理函数
2
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
3
// 匹配函数调用的正则
4
const fnInvokeRE = /\([^)]*?\);*$/
5
// 是否是 test.test todoSomething 这种直接传入函数名的调用
6
const simplePathRE =
7
/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
8
9
function genHandler(
94 collapsed lines
10
handler: ASTElementHandler | Array<ASTElementHandler>
11
): string {
12
// 如果没有传入值,返回空函数
13
// 比如: @click.stop @click.prevent
14
if (!handler) {
15
return 'function(){}'
16
}
17
18
// 如果传入的是数组, 递归调用 genHandler 处理每一个函数, 最后返回处理函数的字符串数组
19
if (Array.isArray(handler)) {
20
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
21
}
22
23
// 如果是普通函数名, 比如 @click="test" @click="utils.test"
24
const isMethodPath = simplePathRE.test(handler.value)
25
// 如果是在模板中直接写的函数
26
// 比如: @click="() => console.log(1)"
27
// 又比如: @click="function () { console.log(1) }"
28
const isFunctionExpression = fnExpRE.test(handler.value)
29
// 如果去掉最后一次函数调用的括号后, 是一个普通的函数, 则说明可能是在运行时直接传的参数
30
// 比如: test(row) 会被替换为 test
31
// 又比如: test(row)(123) 会被替换为 test(row)
32
// 在被替换了以后, 判断是否是一个普通的函数
33
const isFunctionInvocation = simplePathRE.test(
34
handler.value.replace(fnInvokeRE, '')
35
)
36
37
// 如果不包含事件修饰符
38
if (!handler.modifiers) {
39
// 如果它是一个普通的函数名或者是 inline function 则直接返回
40
if (isMethodPath || isFunctionExpression) {
41
return handler.value
42
}
43
// 否则返回一个函数, 并判断传入的句柄的类型
44
return `function($event){${
45
// 如果它本身是一个函数调用, 则直接将这个函数调用作为返回值, 否则可能是个非函数的值(show = false), 需要将其包裹在函数调用中
46
isFunctionInvocation ? `return ${handler.value}` : handler.value
47
}}` // inline statement
48
} else {
49
// 最终输出的code
50
let code = ''
51
// 包含注入了修饰符代码的code
52
let genModifierCode = ''
53
54
const keys: string[] = [] // 保存所有的事件修饰符
55
56
// 遍历所有事件修饰符
57
for (const key in handler.modifiers) {
58
// 如果是预定义好的修饰符
59
if (modifierCode[key]) {
60
// 将其转换为对应的代码
61
genModifierCode += modifierCode[key]
62
// 如果是按键code作为修饰符, 比如 @click.ctrl @click.enter
63
if (keyCodes[key]) {
64
keys.push(key)
65
}
66
} else if (key === 'exact') {
67
// 如果是 exact 修饰符, 则表示是按照精确的修饰符来触发事件
68
const modifiers = handler.modifiers
69
genModifierCode += genGuard(
70
['ctrl', 'shift', 'alt', 'meta']
71
// 过滤其他的修饰符
72
.filter(keyModifier => !modifiers[keyModifier])
73
// 将剩余的修饰符转换为对应的代码, $event.ctrlKey, $event.shiftKey...
74
.map(keyModifier => `$event.${keyModifier}Key`)
75
// 将上面的代码用 || 连接起来, $event.ctrlKey || $event.shiftKey || $event.altKey || $event.metaKey
76
.join('||')
77
)
78
} else {
79
// 意料之外的修饰符, 可能是自定义的修饰符, 或者是键盘按键的code修饰符
80
keys.push(key)
81
}
82
}
83
// 如果定义了其他修饰符则去匹配键盘的keyCode
84
if (keys.length) {
85
code += genKeyFilter(keys)
86
}
87
// 确保在 keys 筛选后执行prevent和stop等修饰符
88
if (genModifierCode) {
89
code += genModifierCode
90
}
91
// 根据传入的参数种类生成对应的代码, 所以只要使用了至少一个修饰符的话, 事件的调用栈深度会至少增加1曾
92
const handlerCode = isMethodPath
93
? `return ${handler.value}.apply(null, arguments)`
94
: isFunctionExpression
95
? `return (${handler.value}).apply(null, arguments)`
96
: isFunctionInvocation
97
? `return ${handler.value}`
98
: handler.value
99
100
// 生成最终的代码, 提供 $event 参数
101
return `function($event){${code}${handlerCode}}`
102
}
103
}

修饰符的按键如何映射到键盘的keyCode

同时还有 genKeyFilter 函数, 用来生成对按下的按键的判断的 if 代码, 以及 genFilterCode 函数, 用来生成对按下的按键 code 的代码。

1
// KeyboardEvent.keyCode aliases
2
// 修饰符对应的keyCode
3
const keyCodes: { [key: string]: number | Array<number> } = {
4
esc: 27,
5
tab: 9,
6
enter: 13,
7
space: 32,
8
up: 38,
9
left: 37,
58 collapsed lines
10
right: 39,
11
down: 40,
12
delete: [8, 46]
13
}
14
15
// KeyboardEvent.key aliases
16
const keyNames: { [key: string]: string | Array<string> } = {
17
// #7880: IE11 and Edge use `Esc` for Escape key name.
18
// #7880:IE11和Edge使用`Esc`作为Escape键名。
19
esc: ['Esc', 'Escape'],
20
tab: 'Tab',
21
enter: 'Enter',
22
// #9112: IE11 uses `Spacebar` for Space key name.
23
// #9112:IE11使用`Spacebar`作为Space键名。
24
space: [' ', 'Spacebar'],
25
// #7806: IE11 uses key names without `Arrow` prefix for arrow keys.
26
// #7806:IE11使用没有箭头键前缀的键名。
27
up: ['Up', 'ArrowUp'],
28
left: ['Left', 'ArrowLeft'],
29
right: ['Right', 'ArrowRight'],
30
down: ['Down', 'ArrowDown'],
31
// #9112: IE11 uses `Del` for Delete key name.
32
// #9112:IE11使用`Del`作为Delete键名。
33
delete: ['Backspace', 'Delete', 'Del']
34
}
35
36
function genKeyFilter(keys: Array<string>): string {
37
return (
38
// make sure the key filters only apply to KeyboardEvents
39
// #9441: can't use 'keyCode' in $event because Chrome autofill fires fake
40
// key events that do not have keyCode property...
41
42
// 确保键过滤器仅适用于KeyboardEvents
43
// #9441:不能在$event中使用'keyCode',因为Chrome自动填充会触发没有keyCode属性的伪事件……
44
`if(!$event.type.indexOf('key')&&` +
45
`${keys.map(genFilterCode).join('&&')})return null;`
46
)
47
}
48
49
function genFilterCode(key: string): string {
50
const keyVal = parseInt(key, 10)
51
// 如果 keyCode 是数字, @keydown.30="test" 会被编译成 $event.keyCode!==30
52
if (keyVal) {
53
return `$event.keyCode!==${keyVal}`
54
}
55
const keyCode = keyCodes[key]
56
const keyName = keyNames[key]
57
58
// _k 的实现细节见下文:
59
return (
60
`_k($event.keyCode,` +
61
`${JSON.stringify(key)},` +
62
`${JSON.stringify(keyCode)},` +
63
`$event.key,` +
64
`${JSON.stringify(keyName)}` +
65
`)`
66
)
67
}

_k() 与 _d() 方法的实现

_k() 方法的实际实现如下,在 此处 有对其进行赋值target._k = checkKeyCodes,主要用于判断按键是否匹配,_d() 函数则是用于处理动态事件名的: target._d = bindDynamicKeys,代码量比较少,处理的分支情况页不多,直接看代码吧:

1
// /src/core/instance/render-helpers/check-keycodes.ts
2
import config from 'core/config'
3
import { hyphenate, isArray } from 'shared/util'
4
5
function isKeyNotMatch<T>(expect: T | Array<T>, actual: T): boolean {
6
if (isArray(expect)) {
7
return expect.indexOf(actual) === -1
8
} else {
9
return expect !== actual
48 collapsed lines
10
}
11
}
12
13
/**
14
* Runtime helper for checking keyCodes from config.
15
* exposed as Vue.prototype._k
16
* passing in eventKeyName as last argument separately for backwards compat
17
* 从 config 中检查 keyCodes 的运行时辅助程序。
18
* 作为 Vue.prototype._k 暴露
19
* 为了向后兼容,最后一个参数单独传入 eventKeyName
20
*/
21
export function checkKeyCodes(
22
eventKeyCode: number,
23
key: string,
24
builtInKeyCode?: number | Array<number>,
25
eventKeyName?: string,
26
builtInKeyName?: string | Array<string>
27
): boolean | null | undefined {
28
const mappedKeyCode = config.keyCodes[key] || builtInKeyCode
29
if (builtInKeyName && eventKeyName && !config.keyCodes[key]) {
30
return isKeyNotMatch(builtInKeyName, eventKeyName)
31
} else if (mappedKeyCode) {
32
return isKeyNotMatch(mappedKeyCode, eventKeyCode)
33
} else if (eventKeyName) {
34
return hyphenate(eventKeyName) !== key
35
}
36
return eventKeyCode === undefined
37
}
38
39
// /src/core/instance/render-helpers/bind-dynamic-keys.js
40
export function bindDynamicKeys(
41
baseObj: Record<string, any>,
42
values: Array<any>
43
): Object {
44
for (let i = 0; i < values.length; i += 2) {
45
const key = values[i]
46
if (typeof key === 'string' && key) {
47
baseObj[values[i]] = values[i + 1]
48
} else if (__DEV__ && key !== '' && key !== null) {
49
// null is a special value for explicitly removing a binding
50
warn(
51
`Invalid value for dynamic directive argument (expected string or null): ${key}`,
52
this
53
)
54
}
55
}
56
return baseObj
57
}

以上。


CD ..