9.3. 事件绑定

前面花了大量的篇幅介绍了模板上的事件标记在构建AST树上是怎么处理,并且如何根据构建的AST树返回正确的render渲染函数,但是真正事件绑定还是离不开绑定注册事件。这一个阶段就是发生在组件挂载的阶段。有了render函数,自然可以生成实例挂载需要的Vnode树,并且会进行patchVnode的环节进行真实节点的构建,如果发现过程已经遗忘,可以回顾以往章节。Vnode树的构建过程和之前介绍的内容没有明显的区别,所以这个过程就不做赘述,最终生成的vnode如下:

9.3. 事件绑定 - 图1

有了Vnode,接下来会遍历子节点递归调用createElm为每个子节点创建真实的DOM,由于Vnode中有data属性,在创建真实DOM时会进行注册相关钩子的过程,其中一个就是注册事件相关处理。

  1. function createElm() {
  2. ···
  3. // 针对指令的处理
  4. if (isDef(data)) {
  5. invokeCreateHooks(vnode, insertedVnodeQueue);
  6. }
  7. }
  8. function invokeCreateHooks (vnode, insertedVnodeQueue) {
  9. for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
  10. cbs.create[i$1](emptyNode, vnode);
  11. }
  12. i = vnode.data.hook; // Reuse variable
  13. if (isDef(i)) {
  14. if (isDef(i.create)) { i.create(emptyNode, vnode); }
  15. if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  16. }
  17. }
  18. var events = {
  19. create: updateDOMListeners,
  20. update: updateDOMListeners
  21. };

我们经常会在template模板中定义v-on事件,v-bind动态属性,v-text动态指令等,和v-on事件指令一样,他们都会在编译阶段和Vnode生成阶段创建data属性,因此invokeCreateHooks就是一个模板指令处理的任务,他分别针对不同的指令为真实阶段创建不同的任务。针对事件,这里会调用updateDOMListeners对真实的DOM节点注册事件任务。

  1. function updateDOMListeners (oldVnode, vnode) {
  2. // on是事件指令的标志
  3. if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
  4. return
  5. }
  6. // 新旧节点不同的事件绑定解绑
  7. var on = vnode.data.on || {};
  8. var oldOn = oldVnode.data.on || {};
  9. // 拿到需要添加事件的真实DOM节点
  10. target$1 = vnode.elm;
  11. // normalizeEvents是对事件兼容性的处理
  12. normalizeEvents(on);
  13. updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
  14. target$1 = undefined;
  15. }

其中normalizeEvents是针对v-model的处理,例如在IE下不支持change事件,只能用input事件代替。

updateListeners的逻辑也很简单,它会遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听,它即要处理原生DOM事件的添加和移除,也要处理自定义事件的添加和移除,关于自定义事件,后续内容再分析。

  1. function updateListeners (on,oldOn,add,remove###1,createOnceHandler,vm) {
  2. var name, def###1, cur, old, event;
  3. // 遍历事件
  4. for (name in on) {
  5. def###1 = cur = on[name];
  6. old = oldOn[name];
  7. event = normalizeEvent(name);
  8. if (isUndef(cur)) {
  9. // 事件名非法的报错处理
  10. warn(
  11. "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
  12. vm
  13. );
  14. } else if (isUndef(old)) {
  15. // 旧节点不存在
  16. if (isUndef(cur.fns)) {
  17. // createFunInvoker返回事件最终执行的回调函数
  18. cur = on[name] = createFnInvoker(cur, vm);
  19. }
  20. // 只触发一次的事件
  21. if (isTrue(event.once)) {
  22. cur = on[name] = createOnceHandler(event.name, cur, event.capture);
  23. }
  24. // 执行真正注册事件的执行函数
  25. add(event.name, cur, event.capture, event.passive, event.params);
  26. } else if (cur !== old) {
  27. old.fns = cur;
  28. on[name] = old;
  29. }
  30. }
  31. // 旧节点存在,接触旧节点上的绑定事件
  32. for (name in oldOn) {
  33. if (isUndef(on[name])) {
  34. event = normalizeEvent(name);
  35. remove###1(event.name, oldOn[name], event.capture);
  36. }
  37. }
  38. }

在初始构建实例时,旧节点是不存在的,此时会调用createFnInvoker函数对事件回调函数做一层封装,由于单个事件的回调可以有多个,因此createFnInvoker的作用是对单个,多个回调事件统一封装处理,返回一个当事件触发时真正执行的匿名函数。

  1. function createFnInvoker (fns, vm) {
  2. // 当事件触发时,执行invoker方法,方法执行fns
  3. function invoker () {
  4. var arguments$1 = arguments;
  5. var fns = invoker.fns;
  6. // fns是多个回调函数组成的数组
  7. if (Array.isArray(fns)) {
  8. var cloned = fns.slice();
  9. for (var i = 0; i < cloned.length; i++) {
  10. // 遍历执行真正的回调函数
  11. invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
  12. }
  13. } else {
  14. // return handler return value for single handlers
  15. return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
  16. }
  17. }
  18. invoker.fns = fns;
  19. // 返回最终事件执行的回调函数
  20. return invoker
  21. }

其中invokeWithErrorHandling会执行定义好的回调函数,这里做了同步异步回调的错误处理。try-catch用于同步回调捕获异常错误,Promise.catch用于捕获异步任务返回错误。

  1. function invokeWithErrorHandling (handler,context,args,vm,info) {
  2. var res;
  3. try {
  4. res = args ? handler.apply(context, args) : handler.call(context);
  5. if (res && !res._isVue && isPromise(res)) {
  6. // issue #9511
  7. // reassign to res to avoid catch triggering multiple times when nested calls
  8. // 当生命周期钩子函数内部执行返回promise对象是,如果捕获异常,则会对异常信息做一层包装返回
  9. res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
  10. }
  11. } catch (e) {
  12. handleError(e, vm, info);
  13. }
  14. return res
  15. }

如果事件只触发一次(即使用了once修饰符),则调用createOnceHandler匿名,在执行完回调之后,移除事件绑定。

  1. function createOnceHandler (event, handler, capture) {
  2. var _target = target$1;
  3. return function onceHandler () {
  4. //调用事件回调
  5. var res = handler.apply(null, arguments);
  6. if (res !== null) {
  7. // 移除事件绑定
  8. remove$2(event, onceHandler, capture, _target);
  9. }
  10. }
  11. }

addremove是真正在DOM上绑定事件和解绑事件的过程,它的实现也是利用了原生DOMaddEventListener,removeEventListener api

  1. function add (name,handler,capture,passive){
  2. ···
  3. target$1.addEventListener(name,handler,
  4. supportsPassive
  5. ? { capture: capture, passive: passive }
  6. : capture);
  7. }
  8. function remove (name,handler,capture,_target) {
  9. (_target || target$1).removeEventListener(
  10. name,
  11. handler._wrapper || handler,
  12. capture
  13. );
  14. }

另外事件的解绑除了发生在只触发一次的事件,也发生在组件更新patchVnode过程,具体不展开分析,可以参考之前介绍组件更新的内容研究updateListeners的过程。