事件
1. 事件循环
编程模型
JavaScript 引擎运行在主线程(单线程)上,仅负责一行行地解析和执行 js 代码,无法实现不堵塞主线程的异步任务,所以需要借助并行的编程模型
-
【模型 1】多线程阻塞
- 把异步任务放在另一个线程中执行,这样对于每个新的线程,都是阻塞的,但主线程不会阻塞
- 对于这种多线程模型,需要用锁解决多线程访问共享内存的冲突问题
-
【模型 2】事件循环
- 让浏览器作为唯一的主线程,掌管 JavaScript 引擎的调用 和 UI 渲染
- 对于异步任务,需要注册到对应的任务队列中,浏览器每次事件循环,都会找出满足条件的任务执行,这样就能继续往下执行 js 代码和渲染工作
对于浏览器
-
任务队列主要分为如下两种
-
宏任务队列
- setTimeout、setInterval、setImmediate、网络 I/O
- 优先级较低,可能会延迟执行
-
微任务队列
- Promise.then,await、MutationObserver
- 优先级较高,需要尽快执行,所以这些事件在执行过程中,如果队列中又有新的满足条件需要执行,就会持续消费,直到队列为空
-
还有一种任务队列
- requestAnimationFrame
- 它的触发时机总是和浏览器渲染保持一致
-
-
两种任务的执行顺序
-
每执行完
一个宏任务,然后就要把微任务队列执行完,也就是持续消费 -
所以下面的代码输出如下
function test() { console.log("start"); setTimeout(() => { console.log("children2"); Promise.resolve().then(() => { console.log("children2-1"); }); }, 0); setTimeout(() => { console.log("children3"); Promise.resolve().then(() => { console.log("children3-1"); }); }, 0); Promise.resolve().then(() => { console.log("children1"); }); console.log("end"); } test(); // start // end // children1 // children2 // children2-1 // children3 // children3-1 -
代码输出如下
Promise.resolve().then(() => { console.log("Promise1"); setTimeout(() => { console.log("setTimeout2"); }, 0); }); setTimeout(() => { console.log("setTimeout1"); Promise.resolve().then(() => { console.log("Promise2"); }); }, 0); // Promise1 // setTimeout1 // Promise2 // setTimeout2
-
对于 Node.js
-
事件循环基于 libuv 实现,会划分出 6 个阶段
-
timers 阶段
- 执行 setTimeout 和 setInterval 中到期的 callback
- 注意:在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行
-
I/O callbacks 阶段
- 上一轮的少数 I/O callbacks 拖延到这轮完成
-
idle,prepare 阶段
-
poll 阶段
- 内部有 poll 队列,含有 I/O callback
- 进入该阶段时,如果 poll 队列不为空
- 会遍历队列并同步执行,直到队列为空或达到系统限制
- 如果 poll 队列为空
- 同时设定了 timer
- 判断是否有 timer 超时,有的话就回到 timer 阶段执行回调
- 同时没有设定 timer
- 如果有 setImmediate 回调需要执行,会直接进入到下一个 check 阶段
- 如果没有 setImmediate 回调需要执行,就会等待一小段时间,等待新的回调加入队列并立即执行
- 同时设定了 timer
-
check 阶段
- 执行 setImmediate 的 callback
-
close callbacks 阶段
- 执行 clock 事件 的 callback
-
在 6 个阶段
内部执行的任务,属于宏任务- 比如 setTimeout、setInterval、setImmediate、I/O callback
- 这些都有可能因为某个阶段耗时较长而难以到期立即执行
-
在 6 个阶段
之间找机会执行的任务,属于微任务- 比如 process.nextTick(),Promise.then,await
- 这些可以保证在事件循环内得到更及时的执行(因为切换阶段时就有机会执行)
-
-
两种任务的执行顺序
- node10 及以前(比较特殊)
- 轮到 timers 阶段时,会把宏任务队列持续消费完,才会停歇,留给中间阶段执行微任务
- 所以上面的代码执行结果会有些差异
// start // end // children1 // children2 // children3 // children2-1 // children3-1
- node11 及以后
- 和浏览器保持一致
- 每执行一个宏任务,然后就要把微任务队列执行完,也就是持续消费
- node10 及以前(比较特殊)
2. 事件模型
2 种事件模型
-
DOM0事件模型 又称原始事件模型,有两种实现方式://通过元素属性绑定 <button onclick="click()">点我</button>; function click() { //do soming }//获取页面元素,再赋值绑定 const btn = document.getElementById("btn"); btn.onclick = function () { //do something }; //解除事件 btn.onclick = null;缺点:一个 DOM 节点只能绑定一个事件,再绑定就会覆盖
-
DOM2事件模型(W3C 制定的统一标准)-
新增了捕获 capture 和冒泡 bubble 的概念
-
分为三个阶段:
- 事件捕获:事件从 document 向下传播到目标元素,依次检查途径节点是否绑定了监听捕获事件,如果有则执行
- 事件处理:事件到达目标元素,触发监听事件
- 事件冒泡:事件从目标元素向上传播到 document,依次检查途径节点是否绑定了监听冒泡事件,如果有则执行
-
要注意
- 在某种事件(比如鼠标点击或按键盘)发生后,顺序一定是,从顶层开始,先向下被捕获,然后再向上做冒泡
-
小技巧
- 如果事件处理绑定在 document 上,这时候第三个参数就可以决定,该事件处理是 默认落后于底层元素(第三个参数为不给或者给 false)还是 明显优先于底层元素(第三个参数参数给 true)
- 如果默认落后的话,若底层元素和 document 同样能响应这个事件,则可以让底层元素优先响应,然后在底层元素中阻止事件继续冒泡(
e.stopPropagation()),防止后续被 document 重复执行 - 如果设置明显优先的话,若底层元素和 document 同样能响应这个事件,则可以让 document 优先响应,然后在 document 中阻止事件继续被捕获(
e.stopImmediatePropagation()),防止后续被底层元素重复执行 - 这种处理方法在 同一事件的多种处理发生冲突时 会很有用
- 如果默认落后的话,若底层元素和 document 同样能响应这个事件,则可以让底层元素优先响应,然后在底层元素中阻止事件继续冒泡(
- 如果事件处理绑定在 document 上,这时候第三个参数就可以决定,该事件处理是 默认落后于底层元素(第三个参数为不给或者给 false)还是 明显优先于底层元素(第三个参数参数给 true)
-
使用方式:
btn.addEventListener('click', 回调函数,参数3)- 关键是参数 3:
- 若为 true,则事件会在捕获阶段 先执行
- 若为 false,则事件会在冒泡阶段 后执行
- 由于一般不传,默认就是 false,会统一在冒泡阶段执行,这样导致捕获阶段像是完全被忽略,除非你有紧急处理需求
- 关键是参数 3:
-
补充
e.preventDefault()可以阻止事件默认的触发e.stopPropogation()可以阻止事件继续向上冒泡e.stopImmediatePropagation()可以阻止事件继续向下捕获
-
-
IE事件模型- 注意 IE 浏览器
只支持冒泡...了解下就好
- 注意 IE 浏览器
例子
-
捕获
<div class="t3"> document <div class="t2"> html <div class="t1"> body <div class="t0">div</div> </div> </div> </div>var $t0 = document.getElementsByClassName("t0")[0]; var $t1 = document.getElementsByClassName("t1")[0]; var $t2 = document.getElementsByClassName("t2")[0]; var $t3 = document.getElementsByClassName("t3")[0]; $t0.addEventListener( "click", function () { alert("click div"); }, true, ); $t1.addEventListener( "click", function () { alert("click body"); }, true, ); $t2.addEventListener( "click", function () { alert("click html"); }, true, ); $t3.addEventListener( "click", function () { alert("click document"); }, true, );根据捕获事件流模型由外向内的规则,会依次弹出: click document -> click html -> click body -> click div
-
冒泡(把 true 改成 false)
<div class="t3"> document <div class="t2"> html <div class="t1"> body <div class="t0">div</div> </div> </div> </div>var $t0 = document.getElementsByClassName("t0")[0]; var $t1 = document.getElementsByClassName("t1")[0]; var $t2 = document.getElementsByClassName("t2")[0]; var $t3 = document.getElementsByClassName("t3")[0]; $t0.addEventListener( "click", function () { alert("click div"); }, false, ); $t1.addEventListener( "click", function () { alert("click body"); }, false, ); $t2.addEventListener( "click", function () { alert("click html"); }, false, ); $t3.addEventListener( "click", function () { alert("click document"); }, false, );根据冒泡事件流模型由内向外的规则,会依次弹出: click div -> click body -> click html -> click docuement
-
两者都有
<div class="t3"> document <div class="t2"> html <div class="t1"> body <div class="t0">div</div> </div> </div> </div>var $t0 = document.getElementsByClassName("t0")[0]; var $t1 = document.getElementsByClassName("t1")[0]; var $t2 = document.getElementsByClassName("t2")[0]; var $t3 = document.getElementsByClassName("t3")[0]; $t0.addEventListener( "click", function () { alert("click div"); }, false, ); $t1.addEventListener( "click", function () { alert("click body"); }, false, ); $t2.addEventListener( "click", function () { alert("click html"); }, true, ); $t3.addEventListener( "click", function () { alert("click document"); }, true, );先潜入,再探出,所以会依次弹出: click document -> click html -> click div -> click body
3. 事件委托
-
又叫事件代理,指的是利用事件冒泡原理,只需给外层父容器添加事件,若内层子元素有点击事件,则会冒泡到父容器上,由父容器的一个事件统一响应
-
简单说就是:子元素委托它们的父级代为执行事件。
-
例子
- 音乐播放器,不能给每首歌都注册事件,就给外层列表注册一个,同时用
e.target获取到真正触发事件的目标子元素
<ul id="music"> <li>青花瓷</li> <li>东风破</li> <li>双节棍</li> </ul>var $music = document.getElementById("music"); $music.addEventListener( "click", function (e) { if (e.target.nodeName.toLowerCase() === "li") { // 判断目标元素target是否为li元素 var content = e.target.innerHTML; console.log(content); } }, false, ); - 音乐播放器,不能给每首歌都注册事件,就给外层列表注册一个,同时用