1.vue.js 执行时,会调用 eventsMixin
办法,增加事件相干的办法到 Vue 原型上,组件(Vue 实例)能够调用这些事件办法。
- 调用
$on
办法增加组件标签上的自定义事件,事件保留在组件的_events
对象上,对象属性名为事件名称,属性值为事件回调函数汇合。 - 调用
$emit
办法触发保留的自定义事件,依据事件名找到事件回调函数汇合,而后顺次调用事件的回掉函数,并传入$emit
办法中的第一个参数后的所有参数。 - 调用
$off
移除自定义事件,如果传了事件名和回调函数两个参数,则移除该事件中的这个回调函数,如果只传了事件名,则移除该事件的所有回调函数。 - 调用
$once
办法增加组件标签上的自定义事件,该事件触发一次后销毁。该办法中调用$on
办法增加事件,同时封装事件的回调函数,当触发该事件的回调函数时,先调用$off
移除回掉函数,再执行原始的回掉函数。
function eventsMixin(Vue) {
var hookRE = /^hook:/;
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;
};
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;
};
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;
};
Vue.prototype.$emit = function (event) {
var vm = this;
if (process.env.NODE_ENV !== 'production') {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;
};
}
2. 模板中标签上各种事件编译的后果:
-
<button @click="test"> 测试 </button>
中事件编译成{on:{"test":test}}
对象 -
<button @click.stop="test"> 测试 </button>
中事件编译成{on:{"click":function($event){$event.stopPropagation();return test($event)}}}
对象 -
<app @test="test" />
编译成{on:{"test":test}}
对象 -
<app @test="test(1)" />
中事件编译成{"test":function($event){return test(1)}}
对象 -
<app @click.native="test" />
中编译成{nativeOn:{"click":function($event){return test($event)}}
对象
组件选项中的
metheds
中办法都会被从新封装,放弃办法中的this
指向组件(Vue 实例),即事件回调函数this
指向它所在的组件。
3. 依据标签创立节点(VNode 实例)时,标签上原生事件编译成的数据对象保留在节点的 data
属性上。组件标签上的自定义事件编译成的数据对象,保留在节点的组件数据中的 listerners
属性上。
原生事件是指原生标签上的事件或者组件标签上增加了
native
修复符的事件。
// 创立组件节点
function createComponent (
Ctor,
data,
context,
children,
tag
) {if (isUndef(Ctor)) {return}
var baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {Ctor = baseCtor.extend(Ctor);
}
...
var listeners = data.on;// 组件标签上的自定义事件
data.on = data.nativeOn;// 组件标签上的原生事件
...
// return a placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context, // // data 保留原生事件信息
{Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children},// listeners 保留组件标签上自定义事件信息
asyncFactory
);
return vnode
}
3. 调用 updateComponentListeners
创立或者更新组件标签上自定义事件。调用组件 $on
办法增加事件回调函数。增加之前会封装回调函数,将原始回调函数保留到封装后的函数的属性 fns
上,在封装的回调函数中调用fns
。
当事件的回调函数更改时,原生事件也不要从新增加事件监听,只有更新回掉函数上的
fns
属性。
var target;
function add(event, fn) {
// 增加绑定在组件标签上的事件
target.$on(event, fn);
}
function remove$1(event, fn) {
// 移除绑定在组件标签上的事件
target.$off(event, fn);
}
function createOnceHandler(event, fn) {
// 增加绑定在组件标签上的事件,事件触发之后移除
var _target = target;
return function onceHandler() {var res = fn.apply(null, arguments);
if (res !== null) {_target.$off(event, onceHandler);
}
};
}
function updateComponentListeners(vm, listeners, oldListeners) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm); // 创立或者更新自定义事件
target = undefined;
}
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)) {process.env.NODE_ENV !== 'production' && warn('Invalid handler for event"' + event.name + '": got' + String(cur), vm);
} else if (isUndef(old)) {if (isUndef(cur.fns)) {cur = on[name] = createFnInvoker(cur, vm); // 封装回调函数:将原始回调函数绑定封装的回调函数 fn 属性上。}
if (isTrue(event.once)) {cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params); // 增加事件回调函数
} else if (cur !== old) {
old.fns = cur; // 更换原始回调函数
on[name] = old; // 替换为封装的回掉函数
}
}
for (name in oldOn) {if (isUndef(on[name])) {event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
// 封装函数,将原始函数保留在封装函数的 fns 属性上。当原始函数产生变更时,只批改 fns 属性就能够。function createFnInvoker (fns, vm) {function invoker () {
var arguments$1 = arguments;
var fns = invoker.fns;
if (Array.isArray(fns)) {var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
}
}
invoker.fns = fns;
return invoker
}
4. 对于原生事件,在 patch
过程中会调用 updateDOMListeners
创立或者更新事件。Vue 提供了 createOnceHandler$1
、add$1
、remove$2
创立和销毁原生事件的办法。在创立和销毁之前缓存以后节点对应的 DOM 元素(target$1 = vnode.elm
)。
对于组件标签上的原生事件,会默认增加到组件内原生根标签 DOM 上,组件标签节点的
elm
指向的也是组件内原生根标签 DOM。
var target$1;
// 增加一次性原生事件
function createOnceHandler$1(event, handler, capture) {
var _target = target$1; // save current target element in closure
return function onceHandler() {var res = handler.apply(null, arguments);
if (res !== null) {remove$2(event, onceHandler, capture, _target);
}
};
}
// 增加事件到原生 DOM 上
function add$1(name, handler, capture, passive) {if (useMicrotaskFix) {
var attachedTimestamp = currentFlushTimestamp;
var original = handler;
handler = original._wrapper = function (e) {if (e.target === e.currentTarget || e.timeStamp >= attachedTimestamp || e.timeStamp <= 0 || e.target.ownerDocument !== document) {return original.apply(this, arguments);
}
};
}
target$1.addEventListener(name, handler, supportsPassive ? { capture: capture, passive: passive} : capture);
}
// 移除原生 DOM 上的事件
function remove$2(name, handler, capture, _target) {(_target || target$1).removeEventListener(name, handler._wrapper || handler, capture);
}
// 更新原生事件
function updateDOMListeners (oldVnode, vnode) {
...
var on = vnode.data.on || {};// 新标签上的原生事件
var oldOn = oldVnode.data.on || {};// 就标签上的原生事件
target$1 = vnode.elm;// 标签对应的 DOM 元素
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
target$1 = undefined;
}