本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
1. 前言
事件系统在 vue 中是非常常用的, 但是我对它一直是会用的地步, 看过文章和源码, 对它算是有过较为深入的了解, 今天试着看看能不能将它的来龙去脉搞清楚(欢迎评论啊~~)
2. 源码
事件系统由$on
,$once
,$off
,$emit
四个Vue
原型上的方法组成, 是在vue
初始化的时候eventsMixin
混入的
const hookRE = /^hook:/
复制代码
2.1 $on
注册监听事件
Vue.prototype.$on = function (
event: string | Array<string>,
fn: Function
): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
// 如果是数组就递归调用自己, 所以可以写出
// this.$on(
// [
// ['a', 'b'],
// ['cd', 'ef'],
// ],
// fn
// )
// 这样奇怪的代码, 来监听`a`,`b`,`cd`,`ef`四个事件
vm.$on(event[i], fn)
}
} else {
// 把需要监听的事件存放到数组里
// 最后vm._events 会长成这样
// vm._events = {
// a: [fn1, fn2],
// b: [fn3, fn4],
// cd: [fn5],
// ef: [fn6],
// }
// 并且可以看到, 这里并没有对fn去重, 所以
// vue.$on('a', fn)
// vue.$on('a', fn)
// vue.$emit('a')
// fn会执行两次
;(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
复制代码
2.2 $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.fn = fn
vm.$on(event, on)
return vm
}
复制代码
2.3 $off
注销掉监听事件
Vue.prototype.$off = function (
event?: string | Array<string>,
fn?: Function
): Component {
const vm: Component = this
// all
// 如果没有传入参数, 就把存储监听事件的对象置空, 也就意味着删除了所有的监听事件
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// 如果是数组, 递归调用
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
// 这里是, 传入了想要卸载的事件名
const cbs = vm._events[event!]
// 没有回调函数, 啥事不干
if (!cbs) {
return vm
}
// 没有传入需要删除的事件回调函数, 表示将这个事件下的所有事件注销掉
if (!fn) {
vm._events[event!] = null
return vm
}
// specific handler
// 到这里是, 传入了需要卸载的事件名和回调函数, 需要一一将这个事件注册的回调中把需要注销的回调注销掉
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
// 将数组中的这个回调删掉
cbs.splice(i, 1)
break
}
}
return vm
}
复制代码
2.4 $emit
触发注册的事件回调
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// 拿出注册的所有事件
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 将剩余参数传递给回调函数
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
// 一一执行
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
复制代码
Vue
中只有这一套事件系统, 可以看出事件的绑定和触发必须在同一个Vue
实例(组件)上, 那么父子间传值看起来好像是在父组件上绑定的事件, 而在子组件上触发的事件, 到底是怎么回事呢? 下面我来讨论一下
3. 父子间传值
先组装一个简单的父子嵌套组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./assets/vue-2.6.14.js"></script>
</head>
<body>
<div id="app">
<a-btn @test-event="testEventHandler"></a-btn>
</div>
<script>
const ABtn = {
name: 'a-btn',
template: `<button @click="clickBtn">按钮</button>`,
methods: {
clickBtn() {
this.$emit('test-event')
},
},
}
const vue = new Vue({
components: {
ABtn,
},
template: '#app',
methods: {
testEventHandler() {
console.log(this)
},
},
})
vue.$mount('#app')
</script>
</body>
</html>
复制代码
3.1 找出父组件 vnode
我们首先看看
template
生成的渲染函数render
长啥样, 在父组件选项中添加
const options = {
// ... 原有参数
beforeMount() {
console.log(this.$options.render.toString())
// 打印如下渲染函数, 就长下面这样
// function anonymous() {
// with (this) {
// return _c('a-btn', { on: { 'test-event': testEventHandler } })
// }
// }
},
}
复制代码
然后我们再跟一下, 看看渲染函数, 生成的
vnode
长啥样
在
$mount
时会调vm._render
,然后会调到this.$options.render
注意渲染函数
_c
中第二个参数, 跟进去
// steps - 1 找到_c定义
vm._c = function (a, b, c, d) {
return createElement(vm, a, b, c, d, false)
}
// steps - 2 跟进 createElement
function createElement(/* 先不在意参数 */) {
/* 过程也不要在意 */
return _createElement()
}
// steps - 3 跟进 _createElement
function _createElement(/* 先不在意参数 */) {
// tag 是 a-btn
if (typeof tag === 'string') {
if (config.isReservedTag(tag)) {
} else if (/* 这里是 */ true) {
// component 这里要跟进去,
vnode = createComponent()
}
}
return vnode
}
// steps - 4 跟进 createComponent
function createComponent(/* 先不在意参数 */) {
// 从 _c 第二个参数中拿到on
var listeners = data.on
var vnode = new VNode(
'vue-component-' + Ctor.cid + (name ? '-' + name : ''),
data,
undefined,
undefined,
undefined,
context,
{
Ctor: Ctor,
propsData: propsData,
listeners: listeners, // 记住这个是第7个参数
tag: tag,
children: children,
},
asyncFactory
)
return vnode
}
// steps - 5 跟进 VNode
var VNode = function VNode(
tag,
data,
children,
text,
elm,
context,
componentOptions, // 这里
asyncFactory
) {
this.componentOptions = componentOptions
}
复制代码
跟到最后, 我们得到, 父组件的渲染函数生成的
vnode
长这样
const vnode = {
// ... 其它属性
componentOptions: {
listeners: {
'test-event': testEventHandler, // 注意 testEventHandler 是父组件中的处理函数, with(this) 时的this指向的是父组件
},
},
}
复制代码
3.2 怎么把事件监听到子组件上去的
在上面我们已经得到了事件在父组件生成的
vnode
的样子, 下面我们看看最后是怎样把事件监听到子组件上去的
初始子组件
// steps - 1 跟进 初始化
Vue.prototype._init = function (options) {
if (options && options._isComponent) {
// 这里要进入看一下
initInternalComponent(vm, options)
}
// 跟进去
initEvents(vm)
}
// steps - 2 跟进 initInternalComponent
function initInternalComponent(vm, options) {
var opts = (vm.$options = Object.create(vm.constructor.options))
// 拿到父组件vnode
var parentVnode = options._parentVnode
var vnodeComponentOptions = parentVnode.componentOptions
// 拿到父组件上的 事件监听, 保存在了 vm.$options._parentListeners 上
opts._parentListeners = vnodeComponentOptions.listeners
}
// steps - 3 跟进 initEvents
function initEvents(vm) {
// 这里是保存子组件上的所有监听的事件, 刚开始为空, 可以看上面的源码解析部分
vm._events = Object.create(null)
// 拿到从`initInternalComponent`中保存到vm.$options._parentListeners上的父组件上监听的事件
var listeners = vm.$options._parentListeners
if (listeners) {
// 这个跟进去
updateComponentListeners(vm, listeners)
}
}
// steps - 4 跟进 updateComponentListeners
function updateComponentListeners(vm, listeners, oldListeners) {
// 注意这里的target是子组件了
target = vm
// 再进去
updateListeners(
listeners,
oldListeners || {},
add,
remove$1,
createOnceHandler,
vm
)
target = undefined
}
// steps - 5 跟进 updateListeners
function updateListeners(on, oldOn, add, remove$$1, createOnceHandler, vm) {
var name, def$$1, cur, old, event
for (name in on) {
def$$1 = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
if (isUndef(cur)) {
} else if (isUndef(old)) {
// 初始化监听时, oldOn是空对象, 所以全部都走到这里了
// 跟进去
add(event.name, cur, event.capture, event.passive, event.params)
}
}
}
// steps - 6 跟进 add
function add(event, fn) {
// 结束, 子组件通过 $on 监听的事件, 处理函数fn 是在父组件上注册的
target.$on(event, fn)
}
复制代码
4. 写一下例子
4.1 公共事件eventBus
const eventBus = new Vue()
const arrowFn = (...args) => {
// 这里可以传入多个参数
// 使用了箭头函数, 回调时绑定不了this了
console.log(args, this)
}
eventBus.$on('test-arrow', arrowFn)
eventBus.$emit('test-arrow', 'arrow参数1', 'arrow参数2')
// 打印
eventBus.$off('test-arrow', arrowFn)
eventBus.$emit('test-arrow', 'arrow参数1', 'arrow参数2')
// 事件已经被注销了
const commonFn = function (...args) {
// 这里可以传入多个参数
// 这里绑定this指向Vue实例
console.log(args, this)
}
eventBus.$once('test-common', commonFn)
eventBus.$emit('test-common', 'common参数1', 'common参数2')
// 事件被触发一次就被注销了
eventBus.$emit('test-common', 'common参数1', 'common参数2')
复制代码
4.2 父子之间传值
略
5. 最后
这篇文章分析了Vue
事件系统的源码, 并且跟出了父组件上的@
事件其实是绑定在子组件上的. 下面列一下我前面写的几篇文章
欢迎加我微信和我交流呀
yangdonglin520l