Bulandent / blog

高级前端进阶博客,记录个人前端成长之路。包括但不限制于:es6、node、vue、小程序、浏览器、网络、设计模式、算法和数据结构、web安全、性能优化、工程化等。
80 stars 8 forks source link

浏览器专题之事件机制 #13

Open Bulandent opened 3 years ago

Bulandent commented 3 years ago

事件流

在早期 IENetscape 团队在开发第四代浏览器的时候,遇到一个问题:当点击一个按钮的时候,是应该先处理父级的事件呢?还是应该先处理按钮的事件呢?IENetscape 给出了 2 种完全相反的答案,IE 提出事件冒泡的概念,而 Netscape 则支持事件捕获。

事件冒泡

事件冒泡认为事件应该由最具体的元素开始触发,然后层层往父级传播:

事件捕获

而事件捕获则相反,认为最外层的元素应该最先收到事件,然后层层往下级传递:

DOM 事件流

为了在浏览器中兼容这 2 种事件流,在 DOM2 Events 规范中将事件流分为 3 个阶段:事件捕获阶段、到底目标阶段、事件冒泡阶段。

可以通过指定 addEventListener 的第三个参数为 true 来设置事件是在捕获阶段调用事件处理程序,默认是 false 指在冒泡阶段调用事件处理程序。

所有现代浏览器都支持 DOM 事件流,只有 IE8 及更早版本不支持。

事件处理程序

HTML 事件处理程序

就是将事件处理程序直接绑定到 HTML 的属性中:

// 方式一
<div onclick="console.log('hello world')"></div>

方式二
<div onclick="print(event)"></div>
<script>
    function print(e) { }
</script>

HTML 事件处理程序修改事件相对麻烦,可能需要同时修改 HTMLJS,所以大家都不爱使用这种方式绑定事件。

DOM0 事件处理程序

将一个函数赋值给 DOM 元素的一个事件处理程序属性,比如 onclick

let btn = document.getElementById('div')

// 添加事件
btn.onclick = function() { }

// 移除事件
btn.onclick = null

DOM2 事件处理程序

通过 addEventListener 可以添加 DOM2 级别的事件处理程序,它接收 3 个参数:事件名、事件处理程序和 useCapture (它是一个可选参数,是个布尔值,默认为 false 表示在冒泡阶段调用事件处理程序)

let btn = document.getElementById('div')
btn.addEventListener('click', () => {

}, false)

DOM0 事件处理程序的区别:

它有几个注意事项:

let btn = document.getElementById('div')
let handler = function() { }
btn.addEventListener("click", handler)
btn.removeEventListener("click", handler)

IE 事件处理函数

由于 addEventListener 无法兼容 IE8 及更早版本,所以此时就可以使用 attachEvent 添加事件处理程序和用 detachEvent 移除事件处理程序。

let btn = document.getElementById('div')
btn.attachEvent("onclick", function() { })

它有这么几个注意事项:

attachEventdetachEventIE 专属的 API,所以如果有兼容性要求,我们可以写出跨浏览器的事件处理程序:

var EventUtil = {
    addHandler: function(element, type, handler) {
        if (element.addEventListener) {
            element.addEventListener(type, handler, false)
        } else if (element.attachEvent) {
            element.attachEvent("on" + type, handler)
        } else {
            element["on" + type] = handler;
        } 
    },
    removeHandler: function(element, type, handler) {
        if (element.removeEventListener) {
            element.removeEventListener(type, handler, false)
        } else if (element.detachEvent) {
            element.detachEvent("on" + type, handler)
        } else {
            element["on" + type] = null
        }
    }
}

事件对象

通过不同的事件处理程序添加的事件,event 对象的属性略有不同,我们不需要记住他们的差异,只需要在平时写代码的时候养成一个写兼容代码的习惯即可,如下是一个兼容各种 event 对象的事件处理程序:

let handler = function(event) {
    // 事件对象
    let event = event || window.event

    // 目标元素
    let target = event.target || event.srcElement

    // 阻止默认事件触发
    if (event.preventDefault) {
        event.preventDefault()
    } else {
        event.returnValue = false
    }

    // 阻止事件冒泡
    if (event.stopPropagation) {
        event.stopPropagation()
    } else {
        event.cancelBubble = true
    }
}

事件类型

DOM3 Events 定义了如下事件类型:

事件委托

事件委托是指将多个元素上绑定的事件通过利用事件冒泡的原理从而转移到他们共同的父级上去绑定,从而在一定程度上起到优化的作用,有的人也喜欢叫它事件代理。比如在 Vue 中经常会将事件绑定到每个列表项中:

<ul>
    <li v-for="item in list" :key="item" @click="handleClick(item)">{{item}}</li>
</ul>

其实更好的做法是利用事件委托,将事件绑定到 ul 上:

<ul @click="handleClick">
    <li v-for="item in list" :key="item" :data-item="item">{{item}}</li>
</ul>
handleClick(event) {
    let target = event.target
    if (target === 'li') {
        let data = target.dataset.item
    }
}