事件合成知多少

ps: 本文首发于公众号 话说前端

在目前的项目开发中,由于使用的 angular 1.x 来进行应用层面的开发,那么在处理事件的时候就不得不使用框架自身所带的 ng-click指令来进行事件的绑定,使用 ng-click 进行绑定的话,对于长列表,交互事件多的地方来说,不是一个好的选择。因为会绑定很多的事件。于是就寻求以一种合适的事件代理的方式来尝试替代(这里只讨论 click)。所以来做个总结

要完成这件事,不妨先回顾一下事件流的一些知识

如上图所示,当你点击页面上的元素的时候,事件会依次到达这三个阶段,它们分别是:

    事件捕获阶段 -> 目标阶段 -> 冒泡阶段。

单拿出冒泡阶段来说,意味着当你点击了某一个具体的元素后,那么最后你是可以在document这个层级接收到事件的,这就构成了代理的基础。

接下来就要考虑的问题是:

  1. 我在根节点上的监听方法如何响应组件里面定义的回调函数
  2. 我们的写法如何与 ng-click 保持一致。
  3. 实现事件合成的冒泡与阻止冒泡

先来看正常的 ng-click 指令是如何使用的,如下:

<example
  ng-click="test($event,item)"
></example>

所以只需要实现一个指令,接收一个函数即可(这里只讨论属性为函数的场景)。

回调函数的查找

const clickDirective = function () {
  return {
    restrict: 'A',
    link(scope, element, attr) {
      try {
        if (!attr.mmClick) return;
        const { fn, functionName, paramsArr } = service.parseParams(attr.mmClick, scope);
        // eslint-disable-next-line
        element[0][`rcs-${functionName}`] = { fn, paramsArr };
      } catch (e) {
        console.error(e);
      }
    },
  };
};

如上,我们可以在指令的初始化中,将我们获取到的回调函数以及参数以特定名称的方式挂载到 dom 树上
这里以 rcs 字符开头,这样就能在根节点接收的事件中去寻找到这个回调函数并处理

function executeEvent(target, e) {
  const keys = Object.keys(target);
  for (let i = 0; i < keys.length; i++) {
    if (keys[i].indexOf('rcs') === 0) {
      const { fn, paramsArr } = target[keys[i]];
      fn.call(this, ...paramsArr, { e });
    }
    // 处理是否需要冒泡
    if (e.hasOwnProperty('stopBubble') && e.stopBubble) {
      return;
    }
  }
  if (target.parentNode !== null) {
    return executeEvent(target.parentNode, e);
  }
}

可以看到,上面的函数是一种递归的方式,逐级寻找回调函数,并执行它。这样就完成了函数的定义与执行部分。

合成事件的冒泡与阻止冒泡

与此同时,在一个dom层级上,可以在不同的层级绑定事件,这样在冒泡阶段,会依次执行。那么在如上的函数中也体现了这点。

如上的函数,会把事件对象通过参数的形式给到调用方(别问为啥在最后,因为要兼容已有函数定义),

fn.call(this, ...paramsArr, { e });

在回调函数里,只需要设置这个值即可:

$scope.showGroupCard = function (item, chatInfoType, { e }) {
  e.stopBubble = true;
}

当这个值为 true 的时候,那么 executeEvent 就会终止执行,否则会直到 parentNodenull

最后再来看看使用效果:

<example
  mm-click="test(item)"
></example>

其实到这里,这个事件合成简易版就算完成了。虽然是利用的dom自身的层次性,但查找速度完全不用担心。

另外就是关于回调函数里面的 this 的问题,回调函数需要使用箭头函数来穿透才行,问题不大

函数参数解析

其实这里最难的部分在于指令的函数解析上,一个函数它的参数可以有如下几种(可能我还没罗列完):

  • 简单的基本数据类型
  • 变量(这个变量可能来自自己组件的作用域,来自父组件传递下来的作用域,也可能来自根作用域)
  • 参数是一个函数,或者是一个递归函数
  • 参数是一个对象,对象里面有变量
  • 等等

所以基于此,再结合实际的业务场景,约定了如下:

  • 指令的属性只能是一个函数,不接受其他值
  • 只保留了当前作用域变量,当前变量的调用,舍弃了表达式等写法,
  • 函数参数只是当前组件作用域及作用域链上的,如果需要使用表达式或者其他写法,就把它挂载到当前作用域上。

end

万物起于微忽,量变引起质变

阅读 89

推荐阅读
云天的前端之路
用户专栏

我在前端漫步,你来吗?

1 人关注
5 篇文章
专栏主页