- 9.3. 事件绑定
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 variableif (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方法,方法执行fnsfunction 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 handlersreturn 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的过程。
