Appearance
1.前言
大家可能会好奇现在Vue都出到3了,为什么还会有这篇文章?这篇文章又在是讲述什么内容?
其实写这篇文章的初衷是对自己的vue2框架使用经历的一次总结(涉及部分源码分析),也是作为一名API工程师
的反思(每次查看文档都会发现新大陆,知其然不知其所以然),也方便新入坑的Vuer
查漏补缺;
读完整篇文章,你可能收获到:
1.组件内的发布/订阅模式
注意点:
- 1.本篇内的Vue2版本特指vue 2.6版本,当前Vue2最新版本为2.7版本,对比源码前请确保代码已checkout到对应的版本tag;
2.组件内的发布/订阅模式
发布/订阅模式,又称观察者模式,在对象上存储一对多的关系,实现触发指定的发布事件($emit),其对应的多个订阅回调($on)立即被执行,具体在Vue组件中的实现是Vue实例方法/事件中的$on、$once、$emit、$off这些API,$emit
这个API我们很熟悉了,常用于父子组件通讯,当然,在实现跨组件通讯中使用到的eventBus
(参考vue篇之事件总线(EventBus))中,上述的API都有使用;
在Vue2中,所有的组件都继承于Vue这个基类,Vue基类上又实现了上述的几个API,观察源码,我们可以得知,在每个组件被实例化的时候,都会执行eventsMixin
来绑定发布/订阅中相关的订阅事件,通过打印组件的this,可以发现,在this上存在_events
对象用作发布/订阅的存储;源码分析如下:
// vue2源码: src/core/instance/event.js
// 以下为伪代码,以表意为主,与源码存在出入,具体实现细节请自行查阅源码
// 初始化代码的时候,为组件创建事件存储器_events
function initEvent(vm) {
// ...
vm._events = Object.create(null);
vm._hasHookEvent = false;
// ...
}
// 实现发布/订阅相关的API
function eventsMixin(Vue) {
// $on是往_events对象上添加键名为事件名,键值为回调事件数组的属性
Vue.prototype.$on = function(eventName, callbackFn) {
const vm = this;
// 往_event中加入$on监听的指定事件,及其指定的回调
(vm._events[event] || (vm._events[event] = [])).push(callbackFn);
if (/^hook:/.test(event)) {
vm._hasHookEvent = true;
}
}
// $emit读取_events上对应的属性(事件名),拿到回调事件数组后,一一执行
Vue.prototype.$emit = function (eventName) {
const vm = this;
// 读取$emit的入参
const args = Array.from(argument).slice(1);
// 读取事件存储器上的指定事件的相关订阅回调
const cbs = vm._events[eventName] || [];
// 一一触发回调
cbs.forEach(cb => {
cb.apply(vm, args);
});
}
// $off读取_events上对应的属性(事件名),拿到回调事件数组后,删除指定的回调
Vue.prototype.$off = function(eventName, callbackFn) {
const vm = this;
// 不传参则全部清空
if (!arguments.length) {
vm._events = {};
return;
}
const cbs = vm._events[eventName];
cbs.splice(cbs.findIndex(cb => cb === callbackFn), 1);
}
// $once是$on和$off的结合,第一回调执行之后通过$off取消监听
Vue.prototype.$once = function (eventName, callbackFn) {
const vm = this;
// 巧妙的包了一层
function on () {
vm.$off(eventName, on);
fn.apply(vm, arguments);
}
on.fn = callbackFn;
vm.$on(eventName, on);
return vm;
}
}
组件内的发布/订阅模式的实现思路如上所述了,基本就是_events上操作存值、读值、执行、删除(CRUD),有趣的是,Vue2实现的组件发布/订阅模式是直接在每个组件上暴露出相关API供Vuer
任意使用,但Vue3上是直接屏蔽掉这部分功能,据官网Vue3迁移策略的说法是全局或任意组件内中使用事件总线会造成令人头疼的维护问题,应该尽量避免使用(在实际应用上确实存在维护问题,尤其是一些莫名其妙的事件命名[PS:不写备注的同事:不是在说我!]),而采取更为通用的父子组件通讯、provide/inject、状态机等等之类便于维护的通讯方式;
也许有小伙伴留意到上述代码中,组件上除了挂载_events
属性之外,还有_hasHookEvent
属性的实现,该属性是一个标志位,表示该实例化组件是否存在hookEvent
,如存在,则每个生命周期执行时,都会调用一下$emit('hook:生命周期')
,这也是为嘛我们可以在组件内使用$once('hook:生命周期')
或者父组件中使用<Child @hook:生命周期="cb">
的原因,实现代码如下:
// vue2源码: src/core/instance/lifecycle.js
// 生命周期调用函数
function callHook(vm, hookName, args, setContext) {
// ...
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
// ...
}