JsWeb前端之路让前端飞

前端面试题——事件代理 delegate 的实现(一)

2017-06-25  本文已影响157人  ac68882199a1

当我们需要监听某个元素被点击的时候,我们会给这个元素添加事件监听器

事件监听器只会绑定到当前 DOM 中已有的元素上,而实际需求中,往往会有临时渲染出来的元素也需要监听事件?那这时候改怎么办呢?难道在每次页面渲染结束后,都再绑定一次事件监听吗?

什么是 事件代理

从字面上来理解,“代理”即将自己要做的事交给别人来做。那么这边的“事件代理”又是什么呢?同样的,如果原本有某个事件 A 是元素 a 的事件,但是 A 事件并不直接由 a 来完成,而是转交给元素 b 来监听并完成

要想彻底理解事件代理的原理,我们需要先了解两个概念:事件捕获事件冒泡

当有下面一段页面结构存在,并且点击了a标签时:

<html>
    <body>
        <div>
            <a></a>
        </div>
    </body>
</html>

事件捕获

事件的触发顺序为 html => body => div => a,即一个事件将会从最不精确的对象 (html) 开始触发,一直到最精确的一个对象 (a),四个字概括就是:由外而内

事件冒泡

与事件捕获刚好相反,触发顺序为 a => div => body => html,由最精确的对象开始触发,一直到最不精确的对象,是 由内而外 式的

绑定事件到 dom 元素

想要绑定事件到元素上,需要先来看下事件监听器的 api :

el.addEventListener(event, action, useCapture)

在这个 api 中,第一个参数是触发事件,如点击事件 ‘click’;第二个参数是事件触发后需要执行的方法;第三个参数的含义是是否使用事件捕获,将该值设为true表示在事件捕获阶段触发

一个对象绑定一个事件

当需要向上文页面结构中的a标签添加一个点击事件时,可以用如下代码:

const aTag = document.querySelector('a')
aTag.addEventListener('click', e => {
    console.log(e)
}) // 默认冒泡事件机制

当该标签被点击时将会执行我们定义好的回调函数,这个回调函数有一个参数 event(此处为了方便将参数名定义为了 e),打印出来会发现这个参数包括了很多很多信息,比如当我们在做拖动效果的时候可能会用到的位置信息的参数等。但是其中的绝大多数参数在这次的事件代理中不会使用到

仔细查看 event 中的属性,会发现有一个属性名为target,我们知道不管是冒泡还是捕获,都会有一个最精确的对象和一个最不精确 的对象,而这个target就是这个最精确的对象,也即我们实际点击到的元素——a

(好奇的小伙伴也可以试着加上最后一个参数true,将事件机制改为捕获,回调中的参数 e 依然会有target,且指向最精确的元素)

那么如果我们将点击事件绑定在a的父级div上再点击a,此时的e.target又会是什么呢?依然是a,因为我们实际点击的对象并没有改变,a依然是最精确的对象。除非点击在a以外的div区域,才会使e.target变成div

多个对象绑定一个事件

<ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>

但是实际情况是,我们往往需要向多个相同的节点中添加事件,而addEventListener这个 api 只能向一个元素添加事件。所以当出现上面这种结构,并且需要向每一个li添加事件时,我们可能会这样做:

// 获取到所有的元素,返回一个包含所有对应元素的类数组
const els = document.querySelectorAll('li')
// 遍历每个元素并未其添加点击事件
Array.prototype.forEach.call(els, el => {
    el.addEventListener('click', e => {
        console.log(e)
    })
})

会发现上面这种方法是通过遍历的方式一个一个地向元素添加所需要的事件,这种方式不得不说是我们非常不愿意见到的

同时,我们只能够向已经存在的元素添加事件,如果通过异步请求数据后重新渲染了页面,新增的节点该如何处理呢?难道再次执行一遍相同的操作吗?显然,这很愚蠢

几遍我们已经确定不会有更多的新元素进入页面,我们也不该使用这种方式达到我们的目的

实现一个事件代理

用过 jQuery 的童鞋都知道,通过$(bigEl).on('click', el, () => {...})的方式添加事件绑定,并且在页面重新渲染后也不需要再次绑定,这就是事件代理的好处:一次绑定,处处通用

那么这是如何实现的呢?这就要用到我们上面说到的target属性了。原理如下:

我们先将需要执行的事件回调绑定在一个必然存在的元素上,比如body,又比如文档节点document,当我们指定的事件,如click发生时,我们就能够获得target属性

由于target指向的永远是我们实际点击到的元素,那么我们就可以通过这个元素来判断是不是我们所需要被点击的元素,从而判断是否执行回调或执行哪一个回调

这样,即使页面上重新增加了元素,我们也不需要对这些元素进行再次绑定

以上面的多li结构为例,代码如下:

document.addEventListener('click', e => {
    if (e.target && e.target.nodeName.toUpperCase == 'LI') {
        // do something u want
        alert('u clicked li element')
    }
})

当然,target不仅能获取到标签名,也能够获取到 class、id 和众多属性,方便我们进行更加精确的判断

通过这种方式,我们还能实现:一个元素绑定多个事件,多个元素绑定一个事件,多个事件绑定多个元素

而所有的事件事实上并不是直接与其对应的元素关联的,而是统一挂载在一个元素上,如 document / body,通过它们间接的触发了事件,实现了事件代理

事件的卸载

jQuery 中提供了一个off方法将已经绑定的事件卸载,通过上面的学习,我们也可以实现这样的事件卸载功能,只是需要绕点弯,至于如何实现,我们下周再说~~~

【结束语:这是最近几周以来最长的一篇,希望你们喜欢!动动你们的食指,不要吝啬你们的喜欢哦!】

扫码关注前端周记公众号,感谢您的支持!
上一篇下一篇

猜你喜欢

热点阅读