Vue之事件相关
前言
本篇文章带来Vue.js的事件机制,具体的分析点如下:
- $emit、$on、$off、$once背后的处理逻辑
- @click形式背后的处理逻辑
具体逻辑梳理
实际上在 Vue初始化 这篇文章中就提及了事件相关实例方法的创建,这里就在具体说下。
在Vue.js文件加载执行,其中会执行eventsMixin函数,该函数的作用就是:
创建事件相关的的原型方法,即$on、$once、$off、$emit
主要源码如下:
function eventsMixin (Vue) {
var hookRE = /^hook:/;
Vue.prototype.$on = function(event, fn) { // codes };
Vue.prototype.$once = function(event, fn) { // codes };
Vue.prototype.$off = function(event, fn) { // codes };
Vue.prototype.$emit = function(event) { // codes };
}
下面就来具体看看每个事件方法背后的处理逻辑(每个方法的处理逻辑不是很复杂,这里会把源码贴出来)。
$emit
该方法用于事件的触发操作
Vue.prototype.$emit = function (event) {
var vm = this;
{
var lowerCaseEvent = event.toLowerCase();
// 如果事件名是大写并且该事件存在
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
// 输出提示
}
}
// 支持事件对应多个处理函数
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
// $emit支持传参
var args = toArray(arguments, 1);
for (var i = 0, l = cbs.length; i < l; i++) {
try {
// 执行对应的事件处理程序
cbs[i].apply(vm, args);
} catch (e) {
handleError(e, vm, ("event handler for \"" + event + "\""));
}
}
}
return vm
};
从上面的源码中可以知道三件事:
- 非小写事件名会有提示
- $emit支持传递多个参数
- 事件的注册中心就是Vue实例的_events变量,该变量保存中当前Vue实例的所有事件
疑问1: _events是如何收集事件的以及在哪里收集的呢?
$on
注册事件
Vue.prototype.$on = function (event, fn) {
var this$1 = this;
var vm = this;
// 支持批量注册事件
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this$1.$on(event[i], fn);
}
} else {
// 注册事件到_events中,并且知道_events是个对象
(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
// 判断是否是hook:开头的事件,这类事件会触发hook:开头的生命周期事件
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
通过$on方法的逻辑可以知道:
- 事件注册是通过emit的疑问)
- events是一个对象,实际上再准确点,vm._events是在init方法中initEvents函数中定义的
vm._events = Object.create(null);
vm._hasHookEvent = false;- 针对hook:开头的事件,实际上在callHook中调用
这里展开下callHook方法的,该方法主要执行生命周期函数,例如beforeCreate、created、mounted等。
callHook中涉及到hook:事件
if (vm._hasHookEvent) {
/*
请注意这里hook就是生命周期函数的名称,$emit会触发它们
hook:beforeCreate
hook:created
...
*/
vm.$emit('hook:' + hook);
}
疑问2:hook:开头的生命周期对应的事件是用来做什么的
如果这里使用者自定义事件名与Vue的生命周期同名,就有意思了,例如:
created() {
this.$on('hook:created', () => {})
}
那实际上按照上面的逻辑,因为callHook中$emit触发这里会自动执行。
$once和$off
只响应一次事件处理
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
};
$off方法的处理就不贴代码了,主要就是调用数组的splice来删除保存的事件处理函数
@click或v-on:click的相关处理
在Vue项目中使用浏览器事件,简单示例:
<button @click="handleClick">
点击
</button>
这里较为详细的解析可参考之前的文章,这里就简要提及下,Vue将template中内容解析构成render函数,即:
with(this){
return _c('div',{
attrs:{"id":"app"}
},[
_c('button',{
on:{"click":handleClick}}
)
])
}
实际上就是Vue官方JSX那边的格式,详情可点击查看 。而这里主要关注点在于vnode -> html这部分解析中,事件相关的注册以及调用处理,这里也暂时只关注事件处理相关(patch相关的后续会专门详细梳理)。
实际上通过源码逻辑的梳理,针对事件相关的处理的入口方法是:
updateDOMListeners
而该方法中主要逻辑如下:
function updateDOMListeners (oldVnode, vnode) {
// 新旧vnode都不存在事件相关
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
normalizeEvents(on);
// 更新事件监听
updateListeners(on, oldOn, add$1, remove$2, vnode.context);
target$1 = undefined;
}
从上面可知,内部实际上是调用了updateListeners方法,具体看看updateListeners的处理逻辑,流程逻辑如下:
从上面的逻辑中需要关注两个步骤的处理逻辑,这里就具体暂开:
- 相关处理步骤
- add$1函数的具体处理逻辑
updateListener中的相关处理
if (isUndef(cur)) {
// 报错提示
} else if (isUndef(old)) {
// 旧vnode没有事件
// 新vnode事件处理函数没有fns属性
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur);
}
// 调用add(即add$1)函数
add(event.name, cur, event.once, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
createFnInvoker函数
function createFnInvoker (fns) {
function invoker () {
var arguments$1 = arguments;
var fns = invoker.fns;
// 可知vue中html中函数定义支持多个函数公共处理
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
cloned[i].apply(null, arguments$1);
}
} else {
// return handler return value for single handlers
return fns.apply(null, arguments)
}
}
// 定义fns属性,实际上fns就是事件处理函数
invoker.fns = fns;
return invoker
}
add$1函数
核心函数,实现函数的注册
function add$1 (
event,
handler,
once$$1,
capture,
passive
) {
handler = withMacroTask(handler);
if (once$$1) { handler = createOnceHandler(handler, event, capture); }
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
从add$1函数的逻辑中,很清晰的知道下面三点信息:
- 使用addEventListener来实现事件绑定
- withMacroTask函数处理了事件处理处理
- 事件只响应一次调用createOnceHandler函数做了特殊处理
function withMacroTask (fn) {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true;
var res = fn.apply(null, arguments);
useMacroTask = false;
return res
})
}
从上面源码可知,就是定义了_withTask方法,而该方法中在事件处理之前设置了全局useMacroTask为true,
而该全局属性只会在nextTick相关的文章。这里估计是处理事件和$nextTick之间调用时间导致新旧vnode导致的问题。
createOnceHandler函数
function createOnceHandler (handler, event, capture) {
var _target = target$1; // save current target element in closure
return function onceHandler () {
var res = handler.apply(null, arguments);
if (res !== null) {
// 从上面add$1可知,该函数必然调用了removeEventListener
remove$2(event, onceHandler, capture, _target);
}
}
}
总结
Vue中事件相关:
- _events中保存的当前实例对象的所有的事件定义
- 对于事件支持多个事件处理函数,也支持一次响应
- hook:开头的对应的生命周期名称事件会被自动执行,例如:hook:created
- @click绑定事件,Vue底层是使用addEventListener来实现事件绑定的