1、事件循环原理
事件队列(至少两个队列)
-
宏任务队列
-
宏任务:一个个离散的、独立的单元
- 创建主文档对象
- 解析html
- 执行全局js
- 更改当前url
- 各种事件
-
-
微任务队列
-
相比宏任务更小的任务,更新应用程序的状态,在浏览器执行其他任务之前执行(重新渲染ui),避免不必要的ui重绘
- promise
- 回调函数
- dom变化
-
事件循环的实现至少有一个宏任务队列和一个微任务队列
两个基本原则
- 一次只处理一个任务
- 一个任务开始后直到运行完成,不会被其他任务打断
工作原理
一次迭代中先检查宏任务队列,有任务就执行,直到该任务执行完成(或队列为空)。
事件循环移动到微任务队列,如果有任务在等待,依次执行,直到执行完全部的微任务。
当微任务队列处理完成并清空时,事件循环会检查是否需要更新ui渲染。如果是,更新ui视图。
至此,当前事件循环结束,之后回到第一个环节,开启新一轮事件循环。-
注意宏任务微任务差别:
- 单次迭代中,宏任务最多只处理一个。而任务中所有微任务队列都会被处理。
细节
- 1.两类任务队列都是独立与事件循环的,意味着任务队列的添加也发生在事件循环之外,如果不这样,导致在执行js代码时,发生的任何事件都将被忽略?
- 2.所有微任务会在下一次渲染之前完成,因为他们的目标是在渲染前更新应用程序状态。
- 3.浏览器通常每秒钟渲染60次页面(帧)。
60fps/s
1s=1000ms
60f/s => 60f/1000ms => 16.6f/ms
也就是说,理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成。
浏览器渲染机制
-
在浏览器完成界面渲染,
进入下一轮事件循环后,
可能发生的3种情况- a:在另一个16ms结束前,事件循环执行到“是否进行渲染”环节。因为更新ui是一个复杂操作,所以如果没有显式的指定需要页面渲染,浏览器可能不会选择在当前的循环中执行ui渲染操作。
- b:在最后一次渲染完成后大约16ms,事件循环执行到“是否进行渲染”环节。在这种情况下,浏览器会进行ui更新,以便用户能感受到顺畅的应用体验;
- c:执行下一个任务(和相关的所有微任务)超过16ms。在这种情况下,浏览器将无法更新页面。如果代码执行在几百毫秒以内,这样的延时可能察觉不到,尤其当页面中没有过多操作时。反而如果耗时过多,或者页面上有动画时,用户会感觉到卡顿。极端情况下,任务执行超过几秒,用户浏览器将会提示”无响应脚本“提示。
2、代码实例
1、宏任务实例(没有微任务,处理完宏任务就可以进行渲染)
<button id="button1"></button>
<button id="button2"></button>
button1.addEventListener('click',function(){...//8ms})
button2.addEventListener('click',function(){...//5ms})
- 执行主线程代码需要15ms
- 第一个单击事件处理器需要运行8ms
- 第一个单击事件处理器需要运行5ms
假设用户手快5ms内点击第一个按钮,12ms点击第二个按钮
1、宏任务队列处理第一个任务:运行js代码,注册两个事件处理器
2、第5、12ms顺序点击两个按钮的操作:添加宏任务处理单击事件1和2,因为当前任务正在进行中,所以不会立即处理点击事件,但是因为添加事件监测和事件添加是独立的,所以不会影响事件添加到宏任务中。
3、15ms后,js运行完毕,渲染操作;执行第一个事件处理器:执行8ms,渲染;执行第二个事件处理器5ms,渲染;
2、宏任务和微任务实例
<button id="button1"></button>
<button id="button2"></button>
button1.addEventListener('click',function(){
Promise.resolve().then(()=>{
//do something 4ms
})
//8ms})
button2.addEventListener('click',function(){
Promise.resolve().then(()=>{
//do something 5ms
})
//5ms})
- 执行主线程代码需要15ms
- 第一个单击事件处理器需要运行8ms
- 第一个单击事件处理器需要运行5ms
假设用户手快5ms内点击第一个按钮,12ms点击第二个按钮
1、宏任务队列处理第一个任务:运行js代码,注册两个事件处理器
2、第5、12ms顺序点击两个按钮的操作:添加宏任务处理单击事件1和2,因为当前任务正在进行中,所以不会立即处理点击事件,但是因为添加事件监测和事件添加是独立的,所以不会影响事件添加到宏任务中。
3、15ms后,js运行完毕,渲染操作;
4、执行第一个事件处理器:执行8ms, ,该宏任务执行完毕后,转到微任务队列中,将微任务全部执行完毕后,本例是4ms,进行是否需要渲染操作;
5、27ms,执行第二个事件处理器5ms,同样产生回调置于微 任务中,宏任务处理完毕,微任务队列处理完毕,进行渲染操作;
注意:在微任务执行之前,不允许重新渲染页面(在处理promise之前)。当微任务队列没有等待当任务时,才可以重新渲染页面。
3、计时器
1、概念
// 计时器是挂载在window对象的方法,与事件类型不同,
// 这些方法不是js本身定义的,而是宿主环境提供的(浏览器或nodejs)
// 计时器作用是将长时间运行的任务分解为不阻塞事件循环的小任务,
// 以阻止浏览器渲染,浏览器渲染过程会讲程序运行缓慢没有反应。
// 延时计时器
// 设置延时计时器
setTimeout: id = setTimeout(fn,delayTime)
// 当延时计时器未触发时,取消计时器
clearTimeout: clearTimeout(id)
// 周期计时器
// 设置周期计时器
setInterval: id = setInterval(fn,delayTime)
// 取消周期计时器
clearInterval: clearInterval(id)
// 注意:delayTime指在指定时间后计时器回调被加入到宏队列等待执行的时间。实际执行时间不确定
2、在事件循环中执行计时器
<!-- 例子 -->
<button id="button"></button>
<script type="text/javascript">
setTimeout(funtcion timeoutHandle(){
// do something... needs 6ms
},10)
setInterval(funtcion ingtervalHandle(){
// do something... needs 8ms
},10)
const button=document.getElementById('button')
button.addEventListener('click',funtcion(){
// do something... needs 10ms
})
// code that runs for 18ms
//假设手快的用户在6ms点击按钮
// 宏队列事件
// 1 执行js主代码 耗时18ms
// 2 6ms 按钮单击处理事件被添加
// 3 10ms 处理延时计时器、周期处理器被添加
// 时间
// -----6ms-----10ms-----18ms-----20ms-----28s-----34ms-----40ms--。。。
// 执行js主代码时,第6ms单击事件被添加
// 10ms,延时、周期处理器被添加
// 18ms,主代码js执行完毕,开始执行微任务,假设当前没有微任务;渲染;执行下一宏任务
// 20ms,执行单击事件处理,此时周期计时器又被触发,但宏任务队列已经有一个周期计时器,浏览器不会创建两个相同的周期计时器;
// 28ms,单击处理事件执行完毕;开始执行延时计时器;18+10
// 30ms,周期计时器又被触发,同样不被添加。
// 34ms,延时计时器执行完毕;开始周期计时器执行 28+6
// 40ms,周期计时器正在执行,不是在等待,一个新的周期计时器被添加到队列中;
// 42ms,周期计时器执行完毕;注意执行空出来了;34+8
// 50ms,添加周期计时器,并执行,58ms执行完毕;
// 60ms,添加周期计时器,并执行,68ms执行完毕;
// 70ms,添加周期计时器,并执行,78ms执行完毕;
// 总结,在10-70ms里,计时器被添加7次,但是实际执行只有5次被执行;
// 延时执行和间隔执行的区别:
// 没搞明白
// 处理计算复杂度高的任务
// 比如创建一个20000行,每行6列的表格,这个操作是惊人的,会导致浏览器挂起一段时间,导致用户无法正操操作。
const tbody = document.querySelector("tbody");//←---查找tbody元素,我们将在其中创建大量的行数
for (let i = 0; i < 100000; i++) { //---- 创建10000行,浏览器空白10s
const tr = document.createElement("tr") ; //←---创建---行
for (let t = 0; t < 6; t++) { //---- 为每一 .行创建6个单元格,每一个单元格有一个文本节点
const td = document.createElement("td");
td.appendChild ( document.createTextNode(i + "," + t));
tr.appendChild(td);
}
tbody.appendChild(tr);//←---将新创建的行添加到父元素中
}
// 使用计时器:将一次性导致浏览器空白的复杂度高的任务分段进行,因为是计时器宏任务,每段执行完毕后可渲染页面;不会导致浏览器挂起,使性能得到优化
const rowCount = 100000; //←— 初始化数据
const divideInto = 4;
const chunkSize = rowCount / divideInto;
let iteration = 0;
const table = document.getElementsByTagName("tbody")[0];
setTimeout(function generateRows() {
const base = chunkSize * iteration; // 计算上一次离开的地方
for (let i = 0; i < chunkSize; i++) {
const tr = document.createElement("tr");
for(let t=0;t<6;t++){
const td = document.createElement("td");
td.appendChild(document.createTextNode((i + base) + "," +t + "," + iteration));
tr.appendChild(td);
}
// console.log(table)
table.appendChild(tr);
}
iteration++;
if (iteration < divideInto) //—— 安排下一个阶段
setTimeout(generateRows, 0);
}, 0); // ←—将超时延迟设置为0来表示下一次迭代应该“尽快”执行,但仍然必须在U I更新之后执行
</script>
3、处理事件
1、事件执行顺序
button.addEventListener('click',function(event){
event.target //指向发生事件的元素
this //指向事件处理器注册的元素
// 一般来说 注册的元素就是事件发生的元素,但总有例外
})
<body id='body'> <!-- 最外层盒子 -->
<div id="out1"> <!-- 外层盒子 -->
<div id="out2"/> <!-- 里层盒子 -->
</div>
</body>
<script type="text/javascript">
body.addEventListener('click',function(event){},false) //1
out1.addEventListener('click',function(event){}) //2
out2.addEventListener('click',function(event){}) //3
// 3个元素注册了3个事件处理器,假设用户点击了里面盒子,因为层级嵌套,显然会触发3个事件处理器,输出3条信息,但3个处理器的执行顺序是怎么样的呢?
// 两大浏览器厂商,Netscape与Microsoft
// 在Netscape事件模型中,事件处理器从顶部元素开始,直到事件目标元素。--捕获
// 在Microsoft中则相反,从目标元素开始,按Dom树向上冒泡。
// W3C委员会设立标准,同时包含两种方式,一个事件处理有两种方式:
// 1、捕获-首先被顶部元素捕获,并依次向下传递(先找true,捕获)
// 2、冒泡-目标元素捕获之后,事件处理转向冒泡,从目标元素向上冒泡(false,冒泡,默认)
// 先上往下捕获,再下往上冒泡
// example
body.addEventListener('click',function(event){})
out1.addEventListener('click',function(event){},true)
out2.addEventListener('click',function(event){},false)
//事件从捕获开始上往下,找到out1,在找到目标out2,转向冒泡找到body。
body.addEventListener('click',function(event){},true)
out1.addEventListener('click',function(event){},false)
out2.addEventListener('click',function(event){},true)
//事件从捕获开始找body,在找到目标out2,最后找到out1
// 点击out2,在out2事件处理器里,this是out2,event.target是out2;
// 在out1处理器里,this是out1,event.target是out1。
// 原因是,this是当前处理器注册的元素;event.target是点击的元素。
// 在嵌套盒子中,单击内部元素,event.target可以检测到实际点击的内部元素,为什么大盒子嵌套小盒子,大盒子没被点击到呢,因为盒子嵌套相当于透明图层的嵌套,内部盒子是在最上层,最前面一层,个人理解;
</script>
2、在祖先元素上代理事件
需求:指出用户在表格中单击的是哪个单元格,并置为红色
- 遍历所有单元格,分别建立处理器,单击时修改颜色,可以解决,但是优雅吗?no
- 优雅的处理方式是,建立唯一的处理器,注册到比单元格更高级的元素上,通过冒泡处理所有的单元格单击时间,单元格是表格的后代元素,通过event.target可以获得被单击的元素,将事件处理器代理到表格上要优雅的多。
<table id="table" style="width:600px" border="1">
<caption><b>我的个人传记</b></caption>
<tr>
<!-- td th 区别???????? -->
<td>书名</td>
<td>作者</td>
</tr>
<tr>
<td>好人好事10000000万篇</td>
<td>禽兽</td>
</tr>
<tr>
<td>学雷锋</td>
<td>禽兽</td>
</tr>
</table>
<script type="text/javascript">
const table=document.getElementById('table');
table.addEventListener('click',function handler2(event){
if(event.target.tagName.toLowerCase()==='td'){
event.target.style.backgroundColor="yellow"
}
})
</script>
3、自定义事件的使用与触发
自定义事件是模拟真实的事件,为了共享代码,创建松耦合的函数。
先定义 再注册 最后触发 也不一定。。。
// 创建方式1: CustomEvent 构造器
var myEvent = new CustomEvent('eventName', {
bubbles: 'true', // 是否冒泡
cancelable: 'true', //是否可以取消默认行为
detail: { title: 'This is title!'},
// 将需要传递的数据写在detail中,以便在EventListener中获取
// 数据将会在event.detail中得到
});
// 事件的监听 创建了事件,就需要对应的事件监听器
document.addEventListener('eventName', function(event){
console.log('数据为:', event.detail); //CustomEvent创建事件,用event.detail取数据
});
// 自定义的事件不是内置事件有操作去触发,需要显式触发
if(window.dispatchEvent) {
window.dispatchEvent(myEvent);
} else {
window.fireEvent(myEvent); // fireEvent函数兼容IE8低版本
}
// 需要特别注意的是,当一个事件触发的时候,如果相应的element及其上级元素没有对应的EventListener,就不会有任何回调操作。
// 创建方式2:
// CustomEnent的另种写法
// 首先创建自定义事件对象
let event = document.createEvent('CustomEvent');
//初始化事件对象
event.initCustomEvent(in String type,in boolean canBuble,in boolean cancelable,in any detail);
/*
param1:要处理的事件名;
param2:事件是否冒泡
param3:是否可以取消默认行为
param4:细节参数
例如:event.initCustom('test',true,true,{a:1,b:2});
*/
window.addEventListener('event_name', function(event){
console.log('得到数据为:', event.detail);
});
// 随后在对应的元素上触发该事件
if(window.dispatchEvent) {
window.dispatchEvent(myEvent);
} else {
window.fireEvent(myEvent);
}
// 书中例子思路
// 封装自定义事件的创建和分发为一个函数,可以调用函数即可创建和触发。
// 调用函数模拟ajax的请求和结束,开始和结束时分别调用创建新的自定义事件
// 监听开始和结束事件,创建业务代码,如风车转动和结束。