浏览器事件机制
事件流
事件流描述了浏览器页面接收事件的顺序。
事件冒泡
IE事件流被称为事件冒泡,这是因为事件被定义为从文档树最深的节点(最具体的元素,最表层的元素)开始触发,然后向上传播至文档。
事件捕获
事件捕获是与事件冒泡相反的机制,事件被定义为从最不具体的节点开始触发,然后最具体的节点最后收到事件。事件捕获实际上是为了最事件到达最终目标前拦截事件。
DOM事件流
DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生, 为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
在DOM事件流中,实际的目标(div)中捕获阶段不会接收到事件,因为捕获阶段从document到html再到body就结束。
而下一阶段,就会在div元素上触发“到达目标阶段”,然后冒泡阶段开始,事件反向传播到文档
大多数支持 DOM 事件流的浏览器实现了一个小小的拓展。虽然 DOM2 Events 规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件
事件处理程序(事件监听器)
事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停 (mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字 以"on"开头,因此 click 事件的处理程序叫作 onclick,而 load 事件的处理程序叫作 onload。有很多方式可以指定事件处理程序
DOM0事件处理程序
在JS中创建事件处理程序的传统方式是把一个函数赋值给DOM元素。这种方式兼容性最好,所有的浏览器都支持这种方法。
每个元素都有它事件处理程序的属性(onxxx),这个属性的值为一个函数
// 以这种方式添加事件处理程序是注册在事件流的冒泡阶段的。
const btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log('Clicked')
}
// 所赋值的函数被视为元素的方法,因此事件处理程序会在元素的作用域中运行,即this指向该元素本身。在事件处理程序中通过this可以访问到元素的属性和方法
btn.onclick = null;
// 通过将事件处理程序属性设置为 null,即可移除通过 DOM0 方式添加的事件处理程序。
// 如果有多个 DOM0 事件处理程序的话,后面的是会把前面的给覆盖掉。只有执行最后一个调用的结果。
DOM2 事件处理程序
DOM2 Events 为事件处理程序的赋值和移除定义了两个方法
-
addEventListener()
-
removeEventListener()。
- 这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序(因为跨浏览器兼容性好,所以事件处理程序默认会被添加到事件流的冒泡阶段)
btn.addEventListener("click", (e) => {
console.log('btn click capture ')
}, true);
btn.addEventListener("click", (e) => {
console.log('btn click bubble ')
});
body.addEventListener("click", (e) => {
console.log('body click capture')
}, true);
body.addEventListener("click", (e) => {
console.log('body click bubble')
});
// DOM2 事件处理程序的一个优点是可以给一个元素添加多个事件处理程序,并按添加的顺序触发。
// body click capture
// btn click capture
// btn click bubble
// body click bubble
使用addEventListener() 添加的事件处理程序只能使用 removeEventLinstener()移除(三个参数均一致才可以);所以,使用匿名函数添加的事件处理程序是不能被移除的。
IE事件处理程序
IE 实现了与 DOM 类似的方法
-
attachEvent()
-
在 IE 中使用 attachEvent()与使用 DOM0 方式的主要区别是事件处理程序的作用域。使用 DOM0 方式时,事件处理程序中的 this 值等于目标元素。而使用 attachEvent()时,事件处理程序是在全 19 局作用域中运行的,因此 this 等于 window。
-
使用 attachEvent()方法也可以给一个元素添加多个事件处理程序
- 不过,与DOM 方法不同,这里的事件处理程序会以添加它们的顺序反向触发。
-
-
detachEvent()
-
使用 attachEvent()添加的事件处理程序将使用 detachEvent()来移除,只要提供相同的参数。 25 与使用 DOM 方法类似,作为事件处理程序添加的匿名函数也无法移除。
const btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ console.log("Clicked"); }) // 这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。 // 因为 IE8 及更早版本只支持事件冒泡,所以使用 attachEvent()添加的事件处理程序会添加到冒泡阶段。
-
事件对象
在 DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。 例如,鼠标操作导致的事件会生成鼠标位置信息,而键盘操作导致的事件会生成与被按下的键有关的信息。所有浏览器都支持这个 event 对象,尽管支持方式不同。
DOM事件对象event
在 DOM 合规的浏览器中,event 对象是传给事件处理程序的唯一参数
在事件处理程序内部,this 对象始终等于 currentTarget 的值,而 target 只包含事件的实际目标
-
preventDefault()方法用于阻止特定事件的默认动作。
- 任何可以通过 preventDefault()取消默认行为的事件,其事件对象的 cancelable 属性都会设置为 true。
-
stopPropagation()方法用于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获或冒泡。
事件委托
“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
// 随意点击一个li,都会冒泡到ul上,因此,只要给ul绑定事件处理程序,就可以完成对li事件的处理
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
// 检查点击对象的event.id,然后执行相应的操作就可以了
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
} });
-
事件委托的优点
- document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded 或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
- 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也可以节省时间。
- 减少整个页面所需的内存,提升整体性能。
-
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress。
-
mouseover 和 mouseout 事件冒泡,但很难适当处理
事件循环
宏任务与微任务
在JavaScript中,任务被分为两种,一种是宏任务,一种叫微任务
- 宏任务:script全部代码、setTimeoutsetInterval、setImmediate、I/O、UI Rendering
- 微任务:Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver
浏览器中的Event Loop
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行
JS调用栈
- JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
同步异步任务
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
JS异步执行机制
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步。
任务队列
- 任务队列
Task Queue,即队列,用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理