Bulandent/blog

浏览器专题之事件机制

Opened this issue · 0 comments

事件流

在早期 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 事件处理程序的区别:

  • addEventListener 可以改变事件流,即可以在捕获阶段触发事件,而 DOM0 是不行的;
  • addEventListener 可以为同一个元素多次添加同一类型的事件处理程序,先添加的事件处理程序会先触发,而 DOM0 如果给同一个元素绑定多个相同类型的事件处理程序的话,则后面添加的会覆盖前面定义的;

它有几个注意事项:

  • 如果不需要在捕获阶段进行拦截操作,则 useCapture 即第三个参可以不传;
  • 通过 addEventListener 添加的事件处理程序只能通过 removeEventListener 移除,而且绑定的事件处理程序必须是同一个。
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() { })

它有这么几个注意事项:

  • 注册的事件名和 DOM0 一样,需要带上 on,比如 onclick
  • 在通过 attachEvent 添加的事件处理程序内部 this 会指向 window,而 DOM0DOM2this 会指向元素本身;
  • addEventListener 一样, attachEvent 也可以针对同一元素多次添加同一个事件类型的处理程序,但是触发顺序是后定义的先触发;
  • 通过 detachEvent 移除事件处理程序的时候,处理函数必须是和注册的同一个,这点和 addEventListener 保持一致;

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 定义了如下事件类型:

  • 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件,比如 onloadresizescrollinputselect 等;
  • 焦点事件(FocusEvent):在元素获得和失去焦点时触发,比如 focusblur
  • 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发,比如 clickmousedownmouseover 等;
  • 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发,比如 mousewheel
  • 输入事件(InputEvent):向文档中输入文本时触发,比如 textInput
  • 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发,比如 keydownkeypress
  • 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发,比如 compositionstart

事件委托

事件委托是指将多个元素上绑定的事件通过利用事件冒泡的原理从而转移到他们共同的父级上去绑定,从而在一定程度上起到优化的作用,有的人也喜欢叫它事件代理。比如在 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
    }
}