—vue实例注册事件的api
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
};
首先判断,传入的event是不是个事件数组,如果是的话,以递归的思路,在vue实例上注册事件。若是单个事件字符串,就会去判断该事件有没有注册过,没有注册过就给该事件初始化一个空数组。之后将fn即回调函数压入数组。
—vue实例注销事件的api
Vue.prototype.$off = function (event, fn) {
var vm = this;
// all
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// array of events
if (Array.isArray(event)) {
for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
vm.$off(event[i$1], fn);
}
return vm
}
// specific event
var cbs = vm._events[event];
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null;
return vm
}
// specific handler
var cb;
var i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break
}
}
return vm
};
分类讨论的思想。
1.针对没有参数传入的情况,直接置空事件列表。作者的思路是,直接由Object.create的方式生产出一个新的存放事件的vue实例属性。
2.若传入的是一个事件数组,那么思路和on是一样的,递归注销。
3.特判部分(specific event部分),就是说如果你这个事件压根没注册过,直接返回。如果你没有传入想要注销的指定事件的回调,直接把对应的事件数组置空。
4.最后就是针对既传入了事件,又传入了参数的情况。作者的思路是逆序遍历数组,用splice删除事件回调。为什么这样做?假如正着遍历,刚删掉一个数,后面的数自动向前移位,那么我们会跳过一个没遍历过的回调。也就是说除了删最后一个回调,正向遍历,每删一个就会跳过一个回调不处理。
Vue.prototype.$once = function (event, fn) {
var vm = this;
function on () {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
显而易见,once底层是使用on和off实现的。将只触发一次的事件回调挂载到on函数上,即用户自己添加的属性,然后把on函数压入对应的事件数组中。当事件被触发时执行on函数内的逻辑,置空事件数组,以apply的形式执行fn。
Vue.prototype.$emit = function (event) {
var vm = this;
{
var 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 + "\"."
);
}
}
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
};
}
function toArray (list, start) {
start = start || 0;
var i = list.length - start;
var ret = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret
}
toArray函数的作用就是根据提供的尺寸大小start,从后向前复制数组长度-start个数组元素。
function invokeWithErrorHandling (
handler,
context,
args,
vm,
info
) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) {
return 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
}
这个函数的逻辑大致就是处理回调。
回到emit主体,首先获取事件对应的回调列表,如果你事件数组长度大于1,它会去调用toArray直接拷贝一份。然后循环遍历数组,处理回调。