一、事件是什么?
事件是用户操作页面时发生的交互动作,像是click/move,除了这个还可以是文档加载,窗口滚动和大小调整。事件被封装成一个event对象,包含了属性和方法。
现代浏览器有三种事件模型:
- DOM0级事件模型:这种模型不会传播,没有事件流的概念,可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数,直接在dom对象上注册事件名字。
- IE事件模型:一次事件有两个过程,事件处理阶段和事件冒泡阶段。事件处理会首先执行目标元素绑定的监听事件,然后从目标元素冒泡到document,依次检查经过的节点是否绑定事件监听函数,如果有则执行。通过attachEvent来添加监听函数,可以添加多个,会依次执行。
- DOM2级事件模型:事件有三个过程,事件捕获、事件触发和事件冒泡,下面详说。
二、事件触发的过程
DOM2级事件模型,事件触发有三个阶段:
- window往事件触发处传播,遇到注册的捕获事件会触发(捕获阶段)
- 触发注册事件
- 触发处往window处传播,遇到注册的冒泡事件会触发(冒泡阶段)
捕获过程即是从window开始,window->document->html->body->div->ul->li,一层层进去,找到触发事件。事件冒泡也是同样的原理,从触发源处一层层往父级元素走,回到window。
事件触发一般来说是按照上面的顺序,但是如果给一个body中的子节点同时注册冒泡事件和捕获事件,事件触发会按照注册的顺序执行。
addEventListener(事件,回调函数,true:捕获阶段/false:冒泡阶段)
//会先打印冒泡,后打印捕获
node.addEventlistener("click",event=>{
console.log("冒泡")
},false)
node addEventListener("click",event=>{
console.log("捕获")
},true)
一般来说,如果只希望事件只触发在目标上,这时候可以使用stopPropagation来阻止事件的进一步传播,多用于阻止事件冒泡,也可以阻止事件捕获。
同样能实现阻止事件的还有stopImmediatePropagation,且能阻止该事件目标执行别的注册事件。
node.addEventListener('click',event=>{
event.stopImmediatePropagation()
console.log('冒泡')
},false)
//点击node只会执行上面的函数,下面这个函数不会执行
node.addEventListener('click',event=>{
console.log('捕获')
},true)
三、事件委托
本质上是利用了浏览器事件冒泡的机制,因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。
场景:给页面的所有a标签添加click事件,可能会想到:
document.addEventListener('click',functrion(e){
if(e.target.nodeName=="A") //返回的是大写
console.log("a")
},false)
但是这些a标签可能包含一些span、img等元素,如果点击到了a标签里的这些元素,就不会触发click事件,毕竟事件是绑定在a上的。
这时使用事件委托,当点击目标时,会逐级向上查找,直到找到a标签为止。
document.addEventListener("click",function(e){
var node=e.target;
while(node.parentNode.nodeName!="BODY"){
if(node.nodeName=="A"){
console.log("a");
break;
}
node=node.parentNode;
}
},false)
四、事件循环 EventLoop
学习JavaScript时大家都会有一个疑问,这个语言明明是个单线程语言,为什么还会有异步?怎么处理异步的?
首先了解一些基本的概念:
- JavaScript有一个main thread主线程和call-stack调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。调用栈是一种后进先出的数据结构,执行代码时,通过不同函数的执行上下文压入栈中来确保代码的有序执行
- 浏览器内核中有多种线程在工作,其中有个叫JS引擎线程,负责解析运行JavaScript脚本。
- JavaScript中的任务分为同步任务和异步任务,同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务会在异步有结果后将注册的回调函数添加到任务队列中,等待主线程空闲时被调到栈中执行。任务队列先进先出。
- 任务队列分为宏任务和微任务。
- 当调用栈中的事件执行完毕,JS引擎会判断微任务队列中是否有任务可以执行,如果有就压入栈,微任务队列执行完毕后再去执行宏任务队列中的任务。
宏任务和微任务
- 微任务:promise的回调、node中的process.next Tick、对DOM变化监听的MutationObserver
- 宏任务:script脚本的执行、setTimeout、setInterval、setImmediate一类的定时事件,I/O操作,UI渲染等
先举个例子:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
- 整体script作为第一个宏任务进入主进程,遇到console.log,输出script start
- 遇到setTimeout,其回调函数被分发到宏任务中
- 遇到promise,then函数被分发到微任务中
- 遇到console.log,输出script end
- 执行微任务,输出promise1,promise2
- 执行宏任务,输出setTimeout
输出顺序是:script start - script end - promise1 - promise2 - setTimeout
再来一个:
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
- 先执行script整体代码,输出script start
- 遇到setTimeout,分发到宏任务
- new Promise中代码立刻执行,输出promise1,然后执行resolve()
- 遇到setTimeout2,分发到宏任务
- 遇到then,分发到微任务
- 输出script end,整体script运行完
- 执行微任务,输出then
- 执行宏任务,输出timeout1,timeout2
输出顺序是:script start - promise1 - script end - then1 - timeout1 - timeout2
总结
学废了学废了