Vue 源码学习之事件机制

前言

写这篇文章的起因是因为今天组里的小伙伴问了我一个比较常见的语法的原理,而我用了这么久的 vue,对该写法早已熟能生巧,但一谈到如何实现,突然愣住了,这玩意儿好像真没注意过,那么是什么问题呢?看下面代码。

1
2
<div @click="onClick">测试</div>
<div @click="onClick($events, '测试')">测试</div>

问:这两种写法区别在哪里?为什么可以用第二种写法?
答:没啥区别。

开个玩笑,当我看到这个时候,本能的猜测,编译的时候,第二种写法做了一层函数的封装,为了弄清到底是不是这样的,带着问题,重温了 vue 相关的源码,也就是这篇文章后面要提到的 vue 事件机制。

编译

我们知道,vue 在挂载实例前,会先对模板做编译,将模板解析成 AST,然后再将 AST 转换为 render 函数,整个过程如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

模板解析的过程不是这篇文章的重点,在这里我们就不赘述了,我们主要来看一下 generate 函数做了啥。

generate

1
2
3
4
5
6
7
8
9
10
11
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new (options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}

generate 函数简单明了,其核心的处理就在于 getElement 中,而对于普通模板处理则在 genData 中,我们来看一下 genData 的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'

// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','

// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}

从代码中不难看出如果有原生事件或事件的时候,会去调用 genHandlers。

genHandlers

genHandlers 函数会遍历 AST 树,拿到 event 对象属性,并根据属性上的事件对象拼接成字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) {
const handlerCode = genHandler(events[name])
if (events[name] && events[name].dynamic) {
dynamicHandlers += `${name},${handlerCode},`
} else {
staticHandlers += `"${name}":${handlerCode},`
}
}
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
return prefix + staticHandlers
}
}

function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
if (!handler) {
return 'function(){}'
}

if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
}

const isMethodPath = simplePathRE.test(handler.value)
const isFunctionExpression = fnExpRE.test(handler.value)
const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ''))

if (!handler.modifiers) {
if (isMethodPath || isFunctionExpression) {
return handler.value
}
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, handler.value)
}
return `function($event){${
isFunctionInvocation ? `return ${handler.value}` : handler.value
}}` // inline statement
} else {
let code = ''
let genModifierCode = ''
const keys = []
for (const key in handler.modifiers) {
if (modifierCode[key]) {
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key)
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
if (genModifierCode) {
code += genModifierCode
}
const handlerCode = isMethodPath
? `return ${handler.value}($event)`
: isFunctionExpression
? `return (${handler.value})($event)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
}
}

经过以上转换后,最终拼接到 render 函数中的字符串如下,分别对应文章开篇的写法,也不难看出,第二种写法确实是封装了一层函数。

1
2
3
"_c('div',{on:{"click":onClick}},[_v("测试")])"
"_c('div',{on:{"click":function($event){return onClick('测试', arguments[0])}}},[_v("测试")])"
"_c('div',{nativeOn:{"click":function($event){return onClick('测试', arguments[0])}}},[_v("测试")])"

总结

由于不知道怎么组织语言,加上文字描述不出自己的心路历程,所以最后产出的这篇文章通篇都是代码,算是相当偷懒了,尴尬。虽然文章很偷懒,但是还是要象征性的总结一下:该篇文章旨在了解 vue 事件绑定时做了些什么事情,另外涉及到一些修饰符,以及自定义事件等,大家可以自行看代码了学习。

写在最后

最后想跟大家分享一点 vue 源码学习的经验,那就是调试。光看代码,很多逻辑都只能靠猜,靠分析,而真正想要理解整个过程,最好的方式还是调试,一行一行代码的跟下去可以让我们更加熟悉整个过程。
如何调试 vue 源码呢?很简单。先到 github 上把 vue 源码拉一份到本地,然后 npm install,再然后 npm run dev
最后,找到 examples 目录,随便找一个示例 html,把里面的<script src="../../dist/vue.min.js"></script>修改成<script src="../../dist/vue.js"></script>,在浏览器上打开该 html 文件即可。

查看评论