9.3. 事件绑定
前面花了大量的篇幅介绍了模板上的事件标记在构建AST
树上是怎么处理,并且如何根据构建的AST
树返回正确的render
渲染函数,但是真正事件绑定还是离不开绑定注册事件。这一个阶段就是发生在组件挂载的阶段。有了render
函数,自然可以生成实例挂载需要的Vnode
树,并且会进行patchVnode
的环节进行真实节点的构建,如果发现过程已经遗忘,可以回顾以往章节。Vnode
树的构建过程和之前介绍的内容没有明显的区别,所以这个过程就不做赘述,最终生成的vnode
如下:
有了Vnode
,接下来会遍历子节点递归调用createElm
为每个子节点创建真实的DOM
,由于Vnode
中有data
属性,在创建真实DOM
时会进行注册相关钩子的过程,其中一个就是注册事件相关处理。
function createElm() {
···
// 针对指令的处理
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
var events = {
create: updateDOMListeners,
update: updateDOMListeners
};
我们经常会在template
模板中定义v-on
事件,v-bind
动态属性,v-text
动态指令等,和v-on
事件指令一样,他们都会在编译阶段和Vnode
生成阶段创建data
属性,因此invokeCreateHooks
就是一个模板指令处理的任务,他分别针对不同的指令为真实阶段创建不同的任务。针对事件,这里会调用updateDOMListeners
对真实的DOM
节点注册事件任务。
function updateDOMListeners (oldVnode, vnode) {
// on是事件指令的标志
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
// 新旧节点不同的事件绑定解绑
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
// 拿到需要添加事件的真实DOM节点
target$1 = vnode.elm;
// normalizeEvents是对事件兼容性的处理
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
target$1 = undefined;
}
其中normalizeEvents
是针对v-model
的处理,例如在IE下不支持change
事件,只能用input
事件代替。
updateListeners
的逻辑也很简单,它会遍历on
事件对新节点事件绑定注册事件,对旧节点移除事件监听,它即要处理原生DOM
事件的添加和移除,也要处理自定义事件的添加和移除,关于自定义事件,后续内容再分析。
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)) {
// 事件名非法的报错处理
warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
// 旧节点不存在
if (isUndef(cur.fns)) {
// createFunInvoker返回事件最终执行的回调函数
cur = on[name] = createFnInvoker(cur, vm);
}
// 只触发一次的事件
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);
}
}
}
在初始构建实例时,旧节点是不存在的,此时会调用createFnInvoker
函数对事件回调函数做一层封装,由于单个事件的回调可以有多个,因此createFnInvoker
的作用是对单个,多个回调事件统一封装处理,返回一个当事件触发时真正执行的匿名函数。
function createFnInvoker (fns, vm) {
// 当事件触发时,执行invoker方法,方法执行fns
function invoker () {
var arguments$1 = arguments;
var fns = invoker.fns;
// 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
}
其中invokeWithErrorHandling
会执行定义好的回调函数,这里做了同步异步回调的错误处理。try-catch
用于同步回调捕获异常错误,Promise.catch
用于捕获异步任务返回错误。
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)) {
// issue #9511
// reassign to res to avoid catch triggering multiple times when nested calls
// 当生命周期钩子函数内部执行返回promise对象是,如果捕获异常,则会对异常信息做一层包装返回
res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
如果事件只触发一次(即使用了once
修饰符),则调用createOnceHandler
匿名,在执行完回调之后,移除事件绑定。
function createOnceHandler (event, handler, capture) {
var _target = target$1;
return function onceHandler () {
//调用事件回调
var res = handler.apply(null, arguments);
if (res !== null) {
// 移除事件绑定
remove$2(event, onceHandler, capture, _target);
}
}
}
add
和remove
是真正在DOM
上绑定事件和解绑事件的过程,它的实现也是利用了原生DOM
的addEventListener,removeEventListener api
。
function add (name,handler,capture,passive){
···
target$1.addEventListener(name,handler,
supportsPassive
? { capture: capture, passive: passive }
: capture);
}
function remove (name,handler,capture,_target) {
(_target || target$1).removeEventListener(
name,
handler._wrapper || handler,
capture
);
}
另外事件的解绑除了发生在只触发一次的事件,也发生在组件更新patchVnode
过程,具体不展开分析,可以参考之前介绍组件更新的内容研究updateListeners
的过程。