• 9.3. 事件绑定

    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的过程。