浏览器&Node的事件循环机制(进阶必备知识)

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

前言

TIP ? 首先javaScript这门语言是一门单线程非阻塞的脚本语言。

单线程: JS引擎是基于单线程事件循环的概念构建的。同一时刻只运行一个代码块在执行,与之相反的是像JAVA和C++等语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不出错。

非阻塞: 当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,比如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞通过事件循环机制Event Loop实现。

Event Loop是什么?

Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

宏队列和微队列

浏览器中主要任务把分为两种: 同步任务、异步任务;

异步任务:Macrotask(宏任务)、Microtask(微任务),宏任务与微任务队列中的任务是随着:任务进栈、出栈、任务出队、进队之间交替进行。

常见的Macrotask(宏任务)

  • setTimeout
  • setInterval
  • setImmediate(node独有)
  • requireAnimationFrame请求动画帧(浏览器独有)
  • UI rendering(浏览器独有)
  • I/O

Microtask(微任务)

  • Process.nextTick(node独有,同步任务执行完就会执行process.nextTick的任务队列,process.nextTick优于Promise.then)
  • Promise.then()
  • Object.observe
  • MutationObserve

浏览器中的Event Loop机制解析

在浏览器的事件循环中,首先要认清楚 3 个角色:函数调用栈宏任务 macro-task) 队列微任务 (micro- task) 队列

当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈。后面每遇到一个函数调用,就会往栈中压入一个新的函数上下文。JS引擎会执行栈顶的函数,执行完毕后,弹出对应的上下文:

image.png

如果你有一堆需要执行的逻辑,它首先需要被推入函数调用栈,后续才能被执行。

循环过程:

  1. 执行全局Script的同步代码;
  2. 检查Microtask queues是否存在执行回调,有就执行microtask任务,直至全部执行完成,任务队列执行栈清空后进入下一步,例如peomise.then().then()两个微任务是依次执行的;
  3. 开始执行macrotask宏任务,Task Queues中按顺序取task执行,每执行完一个task都会检查Microtask队列是否为空(执行完一个Task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有Microtask,然后再进入下一个循环从Task Queue中取下一个Task执行,以此类推。

来一段代码实例:

console.log('1');

async function foo() {
    console.log('13');
    await bar();
    console.log('15');
}

function bar() {
    console.log('14');
}

setTimeout(function () {
    console.log('2');
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5');
    });
});

new Promise(function (resolve) {
    console.log('7');
    resolve();
}).then(function () {
    console.log('8');
});

setTimeout(function () {
    console.log('9');

    new Promise(function (resolve) {
        console.log('11');
        resolve();
    }).then(function () {
        console.log('12');
    });
});

foo();

// 1、7、13、14、8、15、2、4、5、9、11、12
复制代码

第一次事件循环:

当前宏任务:

  • 执行的同步代码为[1、7、13、14]
  • 微任务Queue[8、15]
  • 宏任务Queue[macro1、macro2]

此时输入的结果:
1、7、13、14、8、15

第二次事件循环:

当前宏任务:

  • 执行的同步代码为[2、4]
  • 微任务Queue[5]
  • 宏任务Queue[macro2]

此时输入的结果:
2、4、5

第三次事件循环:

当前宏任务:

  • 执行的同步代码为[9、11]
  • 微任务Queue[12]
  • 宏任务Queue[]

此时输入的结果:
9、11、12

Node中的Event Loop

node中Event Loop是由libuv实现的。

NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段,如下图:

image.png

各个阶段执行的任务如下:

  • timers阶段:这个阶段执行setTimeout和setInterval中定义的回调
  • I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks(少见,略过)
  • idle, prepare阶段:仅node内部使用(可以略过)
  • poll阶段:执行I/O回调,同时还会检查定时器是否到期
  • check阶段:执行setImmediate()设定的回调
  • close callbacks阶段:处理一些“关闭”的回调,执行socket.on(‘close’, ….)这些callbacks

NodeJS的Event Loop过程:

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,优先清空next-tick队列中的任务,随后才会清空其他微任务;
  3. 开始执行macrotask宏任务,每次会尝试清空当前阶段对应宏任务队列里的所有任务;
  4. 步骤3开始,会进入3 -> 2 -> 3 -> 2 ….循环

循环形式: 宏任务队列 -> 微任务队列 -> 宏任务队列 -> 微任务队列 这样交替进行。

console.log('start');

setTimeout(() => {          // callback1
  console.log(1);
  setTimeout(() => {        // callback2
    console.log(2);
  }, 0);
  setImmediate(() => {      // callback3
    console.log(3);
  })
  process.nextTick(() => {  // callback4
    console.log(4);  
  })
}, 0);

setImmediate(() => {        // callback5
  console.log(5);
  process.nextTick(() => {  // callback6
    console.log(6);  
  })
})

setTimeout(() => {          // callback7              
  console.log(7);
  process.nextTick(() => {  // callback8
    console.log(8);   
  })
}, 0);

process.nextTick(() => {    // callback9
  console.log(9);  
})

console.log('end');

// start end 9 1 7 4 8 5 3 6 2
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享