js事件循环机制

avatar

事件循环机制

很多文章都在讨论事件循环机制,当然在面试中这也是必不可缺的话题之一,但是事件循环到底做了什么?你对于事件循环有关的内容又知道多少呢?本文主要围绕这几点进行简单探究,希望大家能从中有所收获。

js事件循环机制

js为什么是单线程?

我们总是说js是单线程执行的,那它为什么要这样设计呢?js作为浏览器脚本语言,它的主要用途是与用户交互,以及操作DOM。这就决定了它只能够是单线程。否则我们假设一下,在当前A线程的某个DOM节点上添加内容,在B线程又删除了该DOM节点,那么此时此刻浏览器是执行A线程的任务还是B线程的任务呢,如果后者先执行,那么该DOM节点已经被删除,又该如何添加内容呢?

异步任务的存在价值

  • 既然js是单线程,运行在浏览器主线程中,承担着诸多的工作,如果全部都使用同步的方式,在一些实际业务场景下,某些http请求过慢或者存在定时任务的时候,此时浏览器处于一直等待的操作,这不仅浪费主线程的时间,另一方面页面无法及时更新,给用户造成卡死现象。
  • 下面例子 setTimeout属于异步任务,js在执行过程中,遇到了异步代码需要耗费一定的时间去处理,但此时并不会堵着下面的代码,而是会接着继续执行。这就是我们常说的异步非阻塞的概念,也是为什么需要异步的原因。
console.log('开始执行');
setTimeout(()=>{
    console.log('setTimeout执行');
},3000);
console.log('结束执行')

以上代码的打印顺序是:

'开始执行'
'结束执行'
'setTimeout执行'
  • 所以浏览器就采用异步的方式来避免,具体方法是某些任务发生时,比如计时器、网络请求、事件监听等。主线程将这异步任务交给其他线程处理,继续执行后面代码,当其他线程完成时,将事先传递的回调加入到任务队列排队,等待主线程调用执行。
  • 在这种模式下,浏览器不会阻塞,那是靠机制来保证浏览器是该种模式运行的呢?

事件循环机制

在讲事件循环机制之前,我们先来看一下这个小故事。

WeChatf77ca67cafdf898d54f3a7dbff23ab74.jpg 小王初来饭店应聘杂役,老板交给他劈柴烧水的任务,让他今天烧100次水,于是小王开始劈一次柴,烧一次水,等水烧开再去劈柴,周而复始。。。但是到了晚上,并没有完成老板交代的任务。于是小王苦思冥想,我完全可以在烧水的间隙去劈柴呀!! 于是第二天,小王按照晚上的想法,在烧水的过程中再去劈柴,发现还没到晚上,就已经完成老板交代的活,顺便还帮厨师做了饭。

看完这则故事,大家也能够看出这就是利用了事件循环机制思想,同步任务依次执行,异步任务(烧水间隙去劈柴)先挂起放进任务队列,等待同步任务执行完毕,在放入到主线程执行。

下图为js事件循环执行机制流程图。

WechatIMG153.jpg

任务队列

小王每天按部就班的劈柴烧水做饭,有一天领导紧急通知,公司要开全员大会让小王也去参加,先不用管手头工作啦。小王就赶紧跑去参加会议,这时候我们能够知道开会优先级>劈柴烧水优先级。 WechatIMG154.jpg

我们可以把小王今天的工作内容看作为一个异步任务,里面存放着优先级高的开会任务,也存放着优先级低的劈柴烧水任务,同样都是今天的工作任务,只不过优先级不同。

异步任务分为微任务和宏任务。我们就可以把开会看作为一个微任务,至于劈柴烧水做饭就是宏任务。

  • 微任务:Promise(then、catch finally)回调、process.nextTick、MutationObserver;
  • 宏任务:setTimeout、setInterval、setImmediate、I/O、UI renderingnew'
微任务存在价值

微任务的存在价值就是当出现优先级高或紧急任务时,可以优先在浏览器渲染之前执行。

输出

既然我们已经明白了js事件循环机制核心,那么看一下这段代码输出来检验一下吧

new Promise(function(resolve){
    console.log('1');
    resolve();
}).then(function(){
    console.log('2')
});
setTimeout(function(){
    console.log('3')
    new Promise(function(resolve){
        console.log('4');
        resolve();
    }).then(function(){
        console.log('下一轮事件循环:5')
    });
},0);
new Promise(function(resolve){
    console.log('6');
    resolve();
}).then(function(){
    console.log('插队的任务:7')
});
console.log('8');

宏任务之你所不知道的setTimeout

setTimeout一定是按照我们设置的延迟时间执行的吗?可以一起来看一下下面的代码,本地运行一下试试看,setTimeout的delay我传入的0,那么就是立即执行的吗 ?

function print(info) {  
let dt = new Date();  
let y = dt.getFullYear();  
let mt = dt.getMonth() + 1;  
let day = dt.getDate();  
let h = dt.getHours(); //获取时  
let m = dt.getMinutes(); //获取分  
let s = dt.getSeconds(); //获取秒  
const ms = dt.getMilliseconds()  
let str =  
"当前时间:" +  
y +  
"年" +  
mt +  
"月" +  
day +  
"日" +  
h +  
"时" +  
m +  
"分" +  
s +  
"秒"+ms+'毫秒'  
  
console.log(info + "————" + str);  
}
function runTime(){
    console.time("本段代码总耗时");
    print("执行开始");
    setTimeout(()=>{
        console.log('setTimeout-------')
    },0)
    for(let i = 0;i<9999999999;i++){}

    print("执行结束");
    console.timeEnd("本段代码总耗时");
}
runTime()

image.png

可以看出本段代码的setTimeout并不是立即执行。setTimeout属于宏任务,它的执行需要同步任务全部执行完毕,当前任务队列是否有微任务,先去执行微任务,最后才会执行宏任务。

感兴趣的还可以试试浏览器打印下面这两段代码,看看有什么新发现。

setTimeout(()=>{  
console.log('timer1')  
},1)  
setTimeout(()=>{  
console.log('timer0')  
},0)
浏览器性能优化API
  1. requestIdleCallback:将在浏览器空闲时期被调用,不会影响其他关键事件。
  • 适用场景:预加载、检测卡顿
  1. rerequestAnimationFrame:在浏览器重新渲染屏幕之前执行,主要用途是按帧对网页进行重绘。
  • 适用场景:推荐做一些流畅动画或大量图表使用
requestIdleCallback例子
  1. requestIdleCallback使用
  • timeRemaining() 方法,能够获取当前浏览器的剩余空闲时间,单位 ms
  • didTimeout,表示是否超时
function work(deadline) { // deadline 上面有一个 ;有一个属性 
  console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
  if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
     // 走到这里,说明时间有余,我们就可以在这里写自己的代码逻辑
  }
  // 走到这里,说明时间不够了,就让出控制权给主线程,下次空闲时继续调用
  requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // 这边可以传一个回调函数(必传)和参数(目前就只有超时这一个参数)

模仿队列出栈,看看控制台输出什么吧。

function sleep(duration){  
const time = new Date().getTime();  
while(new Date().getTime()-time<duration){  
  
}  
}3  
const task =[  
() => {  
console.log('开始执行任务 1')  
sleep(30)  
console.log("已经完成任务 1");  
},  
() => {  
console.log('开始执行任务 2')  
sleep(30)  
console.log("已经完成任务 2");  
},  
() => {  
console.log('开始执行任务 3')  
sleep(30)  
console.log("已经完成任务 3");  
},  
]  
function workLoop(deadline){  
console.log('本帧还剩余'+deadline.timeRemaining()+'ms');  
while ((deadline.timeRemaining()>0||deadline.didTimeout)&&task.length>0){  
work()  
}  
if(task.length>0){  
window.requestIdleCallback(workLoop)  
}  
}  
function work(){  
const workItem = task.shift();  
workItem()  
}  
window.requestIdleCallback(workLoop)