今天来看看vue的事件相关方法,本来想先看模板渲染和虚拟dom相关的内容,但是看了2天之后,感觉那一块内容很多而且难- -,所以先啃这块比较简单一点的骨头好了。
在实例化Vue
的时候,初始化过程会调用一个initEvent
函数,initEvent
export function initEvents (vm: Component) {
// _events对象来缓存事件
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
// 模板事件相关,先不说
// const listeners = vm.$options._parentListeners
// if (listeners) {
// updateComponentListeners(vm, listeners)
// }
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
在初始化事件的方法里,主要是生成了一个vm._events
对象来缓存事件,vm._hasHookEvent
来标识是否有钩子事件
Vue
提供了4个实例方法供我们注册和触发事件。vm.$on、vm.$once、vm.$off、vm.$emit
下面来分别看看它们的实现
# vm.$on
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 如果传入的event是数组,则循环每个event,递归调用$on绑定
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
// 如果vm._events[event]是一个数组,里面储存了触发这个事件时的回调方法定义
// 如果存在就直接push,否则初始化[]
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
// 如果event包含 hook:xx 则标记_hasHookEvent为true
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# vm.$emit
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
// 如果小写形式的event被注册了,但是本身传入的不是小写形式,则友情提示
// tip(
// `Event "${lowerCaseEvent}" is emitted in component ` +
// `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
// `Note that HTML attributes are case-insensitive and you cannot use ` +
// `v-on to listen to camelCase events when using in-DOM templates. ` +
// `You should probably use "${hyphenate(event)}" instead of "${event}".`
// )
}
}
// 该event的回调函数列表
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 将剩余的参数转为数组
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
// 依次调用回调函数
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
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
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
执行以下代码试试
a.$on('say', function(name){
console.log('say' + name)
})
a.$on('say', function(){
console.log('say2')
})
a.$on('sing', function(song){
console.log('sing a song: ' + song)
})
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
可以看到事件注册到了_events
里,然后$emit
触发。而且方法返回的是vm
实例,所以我们还可以进行链式调用
# vm.$off
参数:
{string | Array<string>} event
(只在 2.2.2+ 支持数组){Function} [callback]
用法:
- 移除自定义事件监听器。
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
// 如果没有提供参数,则移除所有事件监听,_events设为空对象
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// 如果传了数组event,则循环递归调用$off移除每个event
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
// 单个event,如果没被注册,则返回
const cbs = vm._events[event]
if (!cbs) {
return vm
}
// 如果没有传入fn,则这个event的回调全部清空
if (!fn) {
vm._events[event] = null
return vm
}
// 如果传了fn, 则循环回调列表删除该fn
if (fn) {
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
// cb.fn见$once
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
}
return vm
}
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
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
# vm.$once
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
// 在$on方法注册event,且重新定义一个回调,在回调里调$off方法移除事件绑定,把原来的fn赋值给on.fn
on.fn = fn
vm.$on(event, on)
return vm
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# hook event
之前说的_hasHookEvent
,用于标识是否有注册过钩子函数,钩子函数的单独注册就是用下面的方法。我感觉这个单独注册钩子函数的主要作用还是用于测试方便。平时使用还是在options
里直接定义的多。
this.$on('hook:created', created)
this.$on('hook:mounted', mounted)
this.$on('hook:destroyed', destroyed)
1
2
3
2
3
生命周期钩子函数等会在指定时期的代码内部调用callHook
方法进行触发
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15