Vue 细节分析 - $emit 和 $on,你的理解正确吗?

概述

之前在和朋友讨论传值方式的时候,发现他对 $on 和 $emit 的理解很混乱,他认为 $emit 会向父组件抛出事件,而 $on 会接收子组件抛来的事件,事实上这个理解是错误的
先说结论,$emit 也只会在当前实例触发事件,$on 也只会捕获当前实例的事件

$on 和 $emit 的运行机制

先看一段源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.prototype.$emit = function (event) {
var vm = this;
// 省略一系列对事件名称格式的判断代码
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};

这是 $emit 函数的实现,首先是通过事件名 event 在 _events 中找到事件的回调函数,如果回调函数存在,那么就执行回调函数。

在这个过程中 vm 为 this,也就是说在组件中使用 this.$emit() 的时候,vm 是指向组件本身的

那么 _events 属性是从哪来的呢?接着看一段 $on 的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(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
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};

关键在 else 块中的第一行,当调用$on函数时,会将事件名 event 作为一个,放入_events对象中,值为回调函数组成的数组(因为可以多次使用 $on 注册事件回调,所以回调函数都会被存入数组中,按注册顺序执行)。

概括

避免有人觉得“太长不看”,这里做一个简单概括:

$on 的功能是在实例上注册事件的回调函数,形式为 事件: 回调数组,回调函数会被放入当前实例 vm_events 对象中。
$emit 的功能是根据参数的事件名,执行相对应的回调函数或回调数组,它会在当前实例 vm_events 对象中找事件对应的回调函数去执行。

所以$emit$on这两个函数涉及的对象都是 vm,和父组件毫无关系。

关于v-on

提到 \$emit 和 ​\$on,那就不能少了 v-on,它也是 Vue 事件机制中重要的一环。

首先,v-on 指令是放在元素或组件中的,在指令的定义函数中参数 el 指向的是指令挂载的组件或元素的。

v-on:click="handleClick" 这种写法会将 handleClick 函数作为 binding.value 传给指令定义函数

那么明确了这两点之后,来看一段代码:

1
2
3
<template>
<child v-on:click="handleClick" />
</template>

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

这是组件 parent 的模版,结合官方文档中的这段话,那就能确定这里的 handleClick 函数是来自于组件parent的(使用 parent 的作用域),组件 child 上的 v-on:click 会监听 child 的事件 click(因为 el 指向 v-on 挂载的组件),所以v-on:click="handleClick"这个语句的意思实际上是:

监听 child 组件上的click事件,事件触发后调用 parent 提供的 handleClick 函数

不应该被理解成组件 parent 去监听 child 的 click 事件,满足条件就触发 handleClick。

再进一步

结合$emit的行为来看 v-on,v-on 实际上是将父组件提供的回调函数 handleClick 放入当前组件 vm(也就是 Child)的_events,所以 vm.$emit(),也能找到父组件传入的 handleClick 函数并执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Parent = new Vue({
template: `
<Child v-on:click="handleClick"></Child>
`,
methods: {
handleClick () {
console.log('click')
}
}
})

const Child = {
template: `<h1>123</h1>`
}

Vue.component('Child', Child)

打印 Parent.$children[0]._events 的结果为{ click: Array(1) },证明 handleClick 已经传递给 Child 了,Child.$emit(‘click’),一样能触发 handleClick 函数。