技术思考 【事件对象】

210 阅读7分钟

事件对象

首先对事件对象有一个清楚的认知,什么是事件对象?

事件对象是一个对象,这个对象里有着触发事件时的所有相关信息。

如何获取事件对象?

在为事件绑定的回调函数中第一个参数就是事件对象,一般命名为 eventeve 。通过输出打印事件对象即可获取所有相关信息,而输出打印 e.target 则获取该事件源。

例子: 鼠标点击事件中,事件对象存了鼠标点击事件的所有相关信息,点击的坐标、触发事件方式标等;如果打印 e.target 则输出当前操作的那个元素,即事件源。如下面代码和图片所示。

document.body.addEventListener('click', function(e) {
    console.log(e);
    console.log(e.target);  // <body></body>
})

点击事件.png

事件对象的妙用

事件对象该如何使用?

首先先看下面的例子,需求是用户按下回车键则把表单内的信息发布出去。

开始解析题目,明确一点,是按下按键发布信息,因此要为表单绑定键盘按下事件 keydown 或键盘弹起事件 keyup 。然后判断是哪个按键,如果是回车键再执行下面的步骤。那么问题来了,按键那么多,如何判断按下的是哪个键?

触发事件后获取所有相关信息,只有事件对象可以做得到,通过事件对象获取当前按下的是哪个按键。

document.body.addEventListener('keydown', function(e) {
    console.log(e);
})

回车.png

如上图所示,判断是否是按下回车键可用 if(e.key == 'Enter'){} ,符合条件执行后面操作即可。

不仅如此,事件对象最大的用处就是可以直接为元素节点的父类绑定事件,再通过判断触发事件的事件源的类名或 id 名是否是自己需要的事件源,如果是则执行下面的操作。通过给父类绑定事件让子元素可以触发的形式,我们称之为事件委托。

总结:

事件对象是个对象,这个对象里有事件触发时的相关信息,可通过判断事件源的类名或 id 名,如果符合则执行后续操作。

事件委托通过事件对象获取到事件源,判断每个子元素来实现不同的操作。

事件委托

何为事件委托?

事件委托即为父元素添加事件,子元素也能触发。其原理为事件流中的事件冒泡,通过事件冒泡的特性,当子元素与父元素有着同样的事件类型,子元素触发后会冒泡导致父元素也触发该事件。

举个简单的例子,过年老板为公司每个部门的员工发年货,一种方法是每个员工单独去领自己的,但是这样麻烦,且不高效。另一种方法是每个部门派一个代表去领部门全部人的,代表领回来后再发给下面的人。

在这里,我们可以将取年货看作一个事件;每个员工看作是需要绑定事件的 DOM 元素;代表看作是这些 DOM 元素的父元素,事件需要绑定在这个父元素上;按照员工分发年货的过程就是事件执行的过程。

事件对象的好处?

通过上面的例子不难看出,每个员工各自去领年货(即每个事件源各自绑定事件)与代表领完部门年货后再分发(为父元素绑定事件通过冒泡机制让子元素进行触发)相比,明显后者的代码冗余量更少,更有利于提高运行性能。

事件委托的应用场景

未来元素

了解到事件委托的原理与好处,事件委托能够用在什么地方?

在回答这个问题之前,我们先看一个案例。

案例.png

代码如下。

<button>加个新盒子</button>
<div class="box">
    <div class="son"></div>
    <div class="sister"></div>
</div>
.box {
    width: 500px;
    height: 500px;
    border: solid;
}

.son,
.sister,
.other {
    width: 100px;
    height: 100px;
    float: left;
}

.son {
    background-color: #f00;
    margin-left: 20px;
}

.sister {
    background-color: #ff0;
    margin-left: 20px;
}

.other {
    background-color: #00f;
    margin-left: 20px;
}

大盒子 div 中有两个小盒子,类名为 son 的小盒子背景色为红色,类名为 sister 的小盒子背景色为黄色,点击按钮后还会生成类名为 other 的蓝色背景色小盒子。

需求为,点击红色小盒子,控制台打印 “单击了 son ”;点击黄色盒子,打印 “单击了 sister ”;点击了生成的蓝色盒子,打印 “单击了 other ”。

乍一看很简单,获取类名的形式拿到每个小盒子,再一一设置单击事件。

let son = document.querySelector('.son')
let sister = document.querySelector('.sister')
let other = document.querySelector('.other')

other.addEventListener('click', function() {
    console.log('other')
})
son.addEventListener('click', function() {
  console.log('son')
})
sister.addEventListener('click', function() {
  console.log('sister')
})

这么做只会报错。

报错.png

或者换种做法,获取所有小盒子,再通过判断类名的方式一一绑定事件。

let div = document.querySelectorAll('div>div')

div.forEach(function(v, i) {
    v.addEventListener('click', function() {
        if ((v.className == 'son')) {
            console.log('单击了son')
        } else if (v.classList.contains('sister')) {
            console.log('单击了sister')
        } else if (v.classList.contains('other')) {
            console.log('单击了other')
        }
    })
})

这么虽然不报错,点击 sonsister 盒子能打印,但是后面生成的 other 小盒子没法触发点击事件。

触发.png 因为 js 的执行机制是先执行完同步任务,再执行异步任务,获取事件源是同步任务,点击事件的回调函数是异步任务。

在用户点击按钮前结构中所有已存在的小盒子已经被获取到了,也只有这些小盒子能够触发点击事件的回调函数,后面生成的小盒子不能再调用点击事件的回调函数。

如果为父容器绑定事件,那么所有的子元素默认情况下都能触发这个事件,不管这个子元素是 已存在,还是将来创建的。

这里是复用事件冒泡的原理,为父容器绑定事件,将来所有子元素都会事件冒泡给这个父容器。

这种将事件绑定给父容器的做法让子元素也能响应事件--就叫做事件委托。

我们如何判断当前元素是我们需要的元素呢?

  1. 我们会为元素添加标识,例如为元素添加一个自定义的类样式
  2. 判断当前元素是否拥有指定的标识,如果有就是我们想要的元素
box.addEventListener('click', function(e) {
    // e.target:获取当前真正触发事件的对象,说白了,当前用户操作那个元素,e.target就是这个元素
    if ((e.target.className == 'son')) {
        console.log('单击了son')
    } else if (e.target.classList.contains('sister')) {
        console.log('单击了sister')
    } else if (e.target.classList.contains('other')) {
        console.log('单击了other')
    } else {
        return
    }
})

11.png

通过事件委托为整体父类绑定事件,利用冒泡原理让其子元素响应,无论是已存在元素还是未来元素,都能够获取触发,再通过判断标识触发子元素相应事件。这就是事件委托的应用场景。

再看下面一个案例。

录入信息.png 点击录入按钮可录入信息,点击删除按钮则删除该行数据。

由于数据不是固定写死,会有未来生成的数据,因此不能用直接获取所有删除按钮的方法,而是为父类 tbody 绑定点击事件,利用事件委托的形式,判断点击的事件对象的类名或 id 名是否是删除按钮,是的话删除该行数据。

carBody.addEventListener('click', function(e) {
   if (e.target.className == 'del') {
        let id = e.target.id
        arr = arr.filter(function(v, i) {
            return v.id != id
        })
        init()
    }
})

复习 filter 方法:

方法 filter 会返回数组内符合条件的元素并生成一个新的数组。例如上面的案例,返回的是数组对象的 id 值不等于自定义属性的删除按钮的 id 值的元素。

众多子元素

如果一个业务(如表单内)有许多子元素,每个子元素的事件处理又不相同,如下图所示。

购物车.png 此时如果一一获取绑定显得代码太多冗余,又要获取复选框按钮,又要获取加号减号按钮,又要获取删除按钮,如果直接为父元素 tbody 绑定点击事件,利用判断事件对象的标识名做相应的业务处理即可。

carBody.addEventListener('click', function(e) {
    if (e.target.className == 'add') {
        let index = e.target.id - 1
        arr[index].count += 1;
        init()
    } else if (e.target.className == 'reduce') {
        let index = e.target.id - 1
        if (arr[index].count == 1) {
            e.target.disable = true
            return
        }
        arr[index].count -= 1;
        init()
    } else if (e.target.className == 'del') {
        let id = e.target.id
        arr = arr.filter(function(ele, index) {
            return ele.id != id
        })
        init()
    }
})

总结

事件委托利用率事件冒泡的机制,为父元素绑定事件,让子元素冒泡触发。

应用场景为子元素业务众多一一绑定代码冗余或者动态生成未来子元素的情况。