1、先贴一下源码
1.1核心代码实现
源码位置: src/core/instance/events.js
代码有删减
/* @flow */
import {
tip,
toArray,
hyphenate,
formatComponentName,
invokeWithErrorHandling
} from '../util/index'
// ...
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
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++) {
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
}
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
}
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
}
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]) {
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}".`
)
}
}
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
}
}
1.2、invokeWithErrorHandling 方法实现
下面看一下 invokeWithErrorHandling 的实现逻辑
源码位置: src\core\util\error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
1.3、分析
-
Vue 的数据相应是依赖于“发布-订阅”模式,emit也是基于这种模式
-
$on 用来收集所有的事件依赖,他会将传入的参数event和fn作为key和value的形式存到vm._events这个事件集合里,就像这样vm._events[event]=[fn]。以便于emit的时候,取出vm._events中存储的方法,调用执行
-
$emit 是用来触发事件的,他会根据传入的event在vm_events中找到对应的事件(vm._events[event]),并执行invokeWithErrorHandling(cbs[i], vm, args, vm, info)
-
最后我们看invokeWithErrorHandling方法可以发现,他是通过handler.apply(context, args)和handler.call(context)的形式执行对应的方法
接着就可以根据 emit的实现方式来写一个eventBus,请继续往下看👇
2、eventBus 实现
eventBus应用场景:跨组件之间的事件通信。
// libs/event.js
// eventBus
// import { ArraySpliceOne } from './utils'
function EventBus() {
this._events = {}
}
EventBus.prototype.on = function (type, fn, ctx = this) {
if (!this._events[type]) {
this._events[type] = []
}
this._events[type].push([fn, ctx])
}
EventBus.prototype.once = function (type, fn, ctx = this) {
function magic() {
this.off(type, magic)
fn.apply(ctx, arguments)
}
magic.fn = fn
this.on(type, magic)
}
EventBus.prototype.off = function (type, fn) {
let _events = this._events[type]
if (!_events) {
return
}
if (!fn) {
this._events[type] = null
return
}
let count = _events.length
while (count--) {
if (_events[count][0] === fn || (_events[count][0] && _events[count][0].fn === fn)) {
// ArraySpliceOne(_events, count)
_events.splice(count, 1)
}
}
}
EventBus.prototype.emit = function (type) {
let events = this._events[type]
if (!events) { return }
let len = events.length
let copyEvents = [...events]
for (let i = 0; i < len; i++) {
let event = copyEvents[i]
let [fn, ctx] = event
if (fn) {
fn.apply(ctx, [].slice.call(arguments, 1))
}
}
}
EventBus.prototype.offAll = function () {
this._events = {}
}
export default new EventBus()
2.1、使用
libs/index.js
// 挂载到vue原型上,以方便使用。(当然也可以不在页面里面导入使用)
import Vue from 'vue'
import EventBus from './event'
Vue.prototype.$EventBus = EventBus
main.js
// main入口文件引入
import './libs'
- 组件内使用
home/a.vue
// home/a.vue
this.$EventBus.emit('optClick', obj)
about/b.vue
// about/b.vue
mounted() {
this.$EventBus.on('optClick', (data) => {
this.optClick(data)
})
}
methods: {
optClick(){}
},
beforeDestroy() {
this.$EventBus.off('optClick')
},
最后
实现逻辑比较简单,重要的是编程思想的学习领会。
🎈🎈🎈
🌹 本篇完,关注我,你会发现一个踏实努力的宝藏前端😊,让我们一起学习,共同成长吧。
🎉 喜欢的小伙伴记得点赞关注收藏哟,回看不迷路 😉
✨ 欢迎大家转发、评论交流
🎊 蟹蟹😊
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。