ToC
修饰符代表了什么
在 vue 开发中时常会看到 .stop
.prevent
.capture
等事件修饰符,这些修饰符是如何被实现的呢?首先我们知道,事件修饰符是在编译阶段被处理的,所以我们需要从编译阶段开始看起。以 Vue2 为例,我们很容易就可以找到这段代码,它在 src/compiler/codegen/events.ts. 修饰符的定义如下:
1// #4868: modifiers that prevent the execution of the listener2// need to explicitly return null so that we can determine whether to remove3// the listener for .once4const genGuard = condition => `if(${condition})return null;`5// ↑ 传入的 condition 为判断条件6
7// 各种修饰符会生成的代码及它的判断条件8const modifierCode: { [key: string]: string } = {9 // ↓ $event 参数是在使用了修饰符或者事件处理器是一个函数调用的话10 // vue 在编译的时候就会额外包装一层函数, 并提供 $event 形参参数11 collapsed lines
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()
的代码比较简单,主要是处理了动态事件名和动态事件句柄,就不做详细解析了:
1export function genHandlers(2 events: ASTElementHandlers,3 isNative: boolean4): string {5 // 判断是否是原生事件6 const prefix = isNative ? 'nativeOn:' : 'on:'7 // 编译后的静态事件名的事件处理函数代码8 let staticHandlers = ``9 // 编译后的动态事件名的事件处理函数代码10 let dynamicHandlers = ``24 collapsed lines
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 + staticHandlers33 }34}
事件处理函数的生成
我们重点看一下 genHandler()
函数的实现:
1// 判断是箭头函数还是 function 这种行内处理函数2const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/3// 匹配函数调用的正则4const fnInvokeRE = /\([^)]*?\);*$/5// 是否是 test.test todoSomething 这种直接传入函数名的调用6const simplePathRE =7 /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/8
9function genHandler(10 handler: ASTElementHandler | Array<ASTElementHandler>93 collapsed lines
11): string {12 // 如果没有传入值,返回空函数13 // 比如: @click.stop @click.prevent14 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) 会被替换为 test31 // 又比如: 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.value42 }43 // 否则返回一个函数, 并判断传入的句柄的类型44 return `function($event){${45 // 如果它本身是一个函数调用, 则直接将这个函数调用作为返回值, 否则可能是个非函数的值(show = false), 需要将其包裹在函数调用中46 isFunctionInvocation ? `return ${handler.value}` : handler.value47 }}` // inline statement48 } else {49 // 最终输出的code50 let code = ''51 // 包含注入了修饰符代码的code52 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.enter63 if (keyCodes[key]) {64 keys.push(key)65 }66 } else if (key === 'exact') {67 // 如果是 exact 修饰符, 则表示是按照精确的修饰符来触发事件68 const modifiers = handler.modifiers69 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.metaKey76 .join('||')77 )78 } else {79 // 意料之外的修饰符, 可能是自定义的修饰符, 或者是键盘按键的code修饰符80 keys.push(key)81 }82 }83 // 如果定义了其他修饰符则去匹配键盘的keyCode84 if (keys.length) {85 code += genKeyFilter(keys)86 }87 // 确保在 keys 筛选后执行prevent和stop等修饰符88 if (genModifierCode) {89 code += genModifierCode90 }91 // 根据传入的参数种类生成对应的代码, 所以只要使用了至少一个修饰符的话, 事件的调用栈深度会至少增加1曾92 const handlerCode = isMethodPath93 ? `return ${handler.value}.apply(null, arguments)`94 : isFunctionExpression95 ? `return (${handler.value}).apply(null, arguments)`96 : isFunctionInvocation97 ? `return ${handler.value}`98 : handler.value99
100 // 生成最终的代码, 提供 $event 参数101 return `function($event){${code}${handlerCode}}`102 }103}
修饰符的按键如何映射到键盘的keyCode
同时还有 genKeyFilter
函数, 用来生成对按下的按键的判断的 if 代码, 以及 genFilterCode
函数, 用来生成对按下的按键 code 的代码。
1// KeyboardEvent.keyCode aliases2// 修饰符对应的keyCode3const keyCodes: { [key: string]: number | Array<number> } = {4 esc: 27,5 tab: 9,6 enter: 13,7 space: 32,8 up: 38,9 left: 37,10 right: 39,57 collapsed lines
11 down: 40,12 delete: [8, 46]13}14
15// KeyboardEvent.key aliases16const 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
36function genKeyFilter(keys: Array<string>): string {37 return (38 // make sure the key filters only apply to KeyboardEvents39 // #9441: can't use 'keyCode' in $event because Chrome autofill fires fake40 // key events that do not have keyCode property...41
42 // 确保键过滤器仅适用于KeyboardEvents43 // #9441:不能在$event中使用'keyCode',因为Chrome自动填充会触发没有keyCode属性的伪事件……44 `if(!$event.type.indexOf('key')&&` +45 `${keys.map(genFilterCode).join('&&')})return null;`46 )47}48
49function genFilterCode(key: string): string {50 const keyVal = parseInt(key, 10)51 // 如果 keyCode 是数字, @keydown.30="test" 会被编译成 $event.keyCode!==3052 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.ts2import config from 'core/config'3import { hyphenate, isArray } from 'shared/util'4
5function isKeyNotMatch<T>(expect: T | Array<T>, actual: T): boolean {6 if (isArray(expect)) {7 return expect.indexOf(actual) === -18 } else {9 return expect !== actual10 }47 collapsed lines
11}12
13/**14 * Runtime helper for checking keyCodes from config.15 * exposed as Vue.prototype._k16 * passing in eventKeyName as last argument separately for backwards compat17 * 从 config 中检查 keyCodes 的运行时辅助程序。18 * 作为 Vue.prototype._k 暴露19 * 为了向后兼容,最后一个参数单独传入 eventKeyName20 */21export 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] || builtInKeyCode29 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) !== key35 }36 return eventKeyCode === undefined37}38
39// /src/core/instance/render-helpers/bind-dynamic-keys.js40export 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 binding50 warn(51 `Invalid value for dynamic directive argument (expected string or null): ${key}`,52 this53 )54 }55 }56 return baseObj57}
以上。