阅读 23

理解EventLoop

js事件循环

  • JS是单线程语言:顺序执行

  • 任务队列

1.png

demo1:

let data = [];
$.ajax({
            url:www.javascript.com,
            data:data,
            success:() => {
                console.log('发送成功!');
            }
})
console.log('代码执行结束');
复制代码
  • setTimeout

demo2:

setTimeout(() => {
            task()
},3000)
sleep(10000)  //sleep为一个同步任务,10000为执行时间
复制代码

注:即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。

  • Promise

Promise 新建后就会立即执行。

  • 宏任务和微任务

macro-task(宏任务):整体代码script,setTimeout,setInterval

micro-task(微任务): Promise

注:在事件循环中,永远先执行可执行的微任务

2.png

demo3:

    setTimeout(function(){
            console.log('1')
  });
  new Promise(function(resolve){
            console.log('2');
            for(var i = 0; i < 10000; i++){
                i == 99 && resolve();
            }
  }).then(function(){
            console.log('3')
  });
  console.log('4');
复制代码

demo4:

console.log('1');
setTimeout(function() {
            console.log('2');
            new Promise(function(resolve) {
                console.log('3');
                resolve();
            }).then(function() {
                console.log('4')
            })
})
new Promise(function(resolve) {
            console.log('5');
            resolve();
}).then(function() {
            console.log('6')
})
setTimeout(function() {
            console.log('7');
            new Promise(function(resolve) {
                console.log('8');
                resolve();
            }).then(function() {
                console.log('9')
            })
})
复制代码

浏览器篇

干讲理论不容易理解,让我们直接以一个例子开始吧:

console.log('start');
setTimeout(() => {
  console.log('timeout');
});
Promise.resolve().then(() => {
  console.log('resolve');
});
console.log('end');
复制代码

我们来分析一下:

  1. 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈(关于执行栈,若不了解请移步之前的文章《JavaScript内存机制之问——数据是如何存储的?》)进行执行,因此先打印start和end
  2. setTimeout 作为一个宏任务放入宏任务队列
  3. Promise.then作为一个为微任务放入到微任务队列
  4. 当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行
  5. 接下来进入到下一个宏任务——setTimeout, 执行

因此最后的顺序是:

start
end
resolve
timeout
复制代码

这样就带大家直观地感受到了浏览器环境下 EventLoop 的执行流程。不过,这只是其中的一部分情况,接下来我们来做一个更完整的总结。

  1. 一开始整段脚本作为第一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  4. 执行浏览器 UI 线程的渲染工作
  5. 检查是否有Web worker任务,有则执行
  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

最后给大家留一道题目练习:

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
});
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0);
console.log('start');
// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
复制代码

nodejs篇

1. 三大关键阶段

nodejs 三个非常重要的执行阶段:

  1. 执行 定时器回调 的阶段。检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeout、setInterval。这个阶段暂且叫它timer
  2. 轮询(英文叫poll)阶段。因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?就是通过'data'、 'connect'等事件使得事件循环到达 poll 阶段。到达了这个阶段后:

如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到timer阶段。

如果没有定时器, 会去看回调函数队列。

  • 如果队列不为空,拿出队列中的方法依次执行

  • 如果队列为空,检查是否有setImmdiate的回调

  • 有则前往check阶段(下面会说)

  • 没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段

  1. check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。

2. 完善

首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的回调阶段。比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。

并且在 check 阶段结束后还会进入到 关闭事件的回调阶段。如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。

梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:

  1. timer 阶段
  2. I/O 异常回调阶段
  3. 空闲、预备状态(第2阶段结束,poll 未触发之前)
  4. poll 阶段
  5. check 阶段
  6. 关闭事件的回调阶段

3. 实例演示

好,我们以上次的练习题来实践一把:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
复制代码

这里我要说,node版本 >= 11和在 11 以下的会有不同的表现。

首先说 node 版本 >= 11的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。

timer1
promise1
time2
promise2
复制代码

而 node 版本小于 11 的情况下,对于定时器的处理是:

若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,那么就将微任务暂时保存,直接去执行新的定时器任务,当新的定时器任务执行完后,再一一执行中途产生的微任务。

动画演示示例

总结一下:

  • 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面

  • 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务

  • 每个回调之后且js执行栈中为空。

  • 每个宏任务结束后。