js异步编程,eventLoop、消息队列都是做什么的? 什么是宏任务,什么是微任务

7,145 阅读5分钟
单线程的JavaScript

JavaScript是一门单线程语言,起因是设计之初js只用来操作dom,对表单进行简单的校验。在这种执行环境简单的情况下,自然就选择了单线程来处理程序。但是单线程如果遇到执行时间较长的程序片段,会拖延甚至阻塞程序的执行,对于用户来说,页面呈现"卡死状态",这是最糟糕的体验。

​ 为了解决上述问题,JavaScript将程序的执行分为同步和异步。

在JavaScript中写异步代码也叫做异步编程,进行异步编程的方式有:

  • 回调函数

  • 事件监听

  • 发布订阅

  • promise

维护异步任务和执行异步任务

异步即将来, 异步任务就是将来执行的任务

js引擎是单线程的,那么异步任务是如何维护的呢?

js引擎负责解析并编译js代码。制定作用域标准,分配内存,创建执行上下文调用栈...。编译好的代码放到运行环境中去运行,而运行环境会维护异步任务。

对浏览器而言,浏览器是多线程的,它可以分配线程去倒计时定时器,发送请求,事件监听等。当定时器中的事件倒计时完毕,将其扔到也是由浏览器维护的消息队列中,当遇到其他异步时,浏览器会分配进程去处理,处理完毕也是扔到消息队列中。等待js引擎去执行。

以上所说的异步任务均是宏任务。

关于异步还有一些有趣的事情。

有趣的异步控制台

console也并非js标准,没有具体的约束和规则去指定console的行为,console的行为是由运行环境决定的。

摘自你不知道的JavaScript(中卷) p141

不同的浏览器和 JavaScript 环境可以按照自己的意愿来实现,有时候这会引起混淆。 尤其要提出的是,在某些条件下,某些浏览器的 console.log(..) 并不会把传入的内容立 即输出。出现这种情况的主要原因是,在许多程序(不只是 JavaScript)中,I/O 是非常低 速的阻塞部分。所以,(从页面 /UI 的角度来说)浏览器在后台异步处理控制台 I/O 能够提 高性能,这时用户甚至可能根本意识不到其发生。

console通常是进行程序调试写的比较多,如果产生了一些迷惑性行为,可以这样做

最好的选择是在 JavaScript 调试器中使用断点, 而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过 JSON.stringify(..)。

当检测到js主线程中的调用栈为空时(主线程会维护一个巨大的匿名函数,这个匿名函数用来执行js代码)。 浏览器早已提供好了用作事件触发的线程,事件触发线程从消息队列中按照队列排序取出一个任务放到执行栈中压栈执行。

执行过程中遇到的问题:

  1. 如果遇到宏任务: 将其给浏览器进行处理,处理完毕放入消息队列中排队。

  2. 如果遇到微任务:将其放到微任务队列中,依次执行,不用去排队。也就是微任务是可以"插队"的。等到微任务队列中的所有微任务全部执行完毕。才会开启下一轮事件循环。

  • 微任务的出现让js的异步处理更加灵活,高效。
  • 例如修改dom: 如果在微任务中修改dom,则在这次事件循环中就可以看到修改后的结果,如果在宏任务中修改,则只能在下次事件循环中看到修改后的结果了。
  • 微任务有微任务消息队列,宏任务有宏任务消息队列[事件队列存放的是异步事件返回结果],但是js的事件循环是唯一一个。
宏任务和微任务有哪些?

宏任务有: scriptsetTimeoutsetIntervalsetImmediate(浏览器暂时不支持,只有IE10支持)、I/OUI Rendering

微任务有: Process.nextTick(node)PromiseMutationObserver

模拟执行一个宏任务

setTimeout(() => {
    console.log('setTimeout');
}, 0);

模拟执行一个微任务

queueMicrotask(() => {
    console.log('queueMicrotask');
}); 

上面两种方式不会创建额外的对象,不会造成浪费(比如通过promise

创建微任务.)

async  function  async1 ()  {
    console.log('async1 start');
    await  async2();
    console.log('async1 end')
}
async  function  async2 ()  {
    console.log('async2')
}
console.log('script start');
setTimeout(function ()  {
    console.log('setTimeout')
},  0);
async1();
new  Promise(function (resolve)  {
    console.log('promise1');
    resolve()
}).then(function ()  {
    console.log('promise2')
});
console.log('script end')

一道面试题

js编译完成,进入浏览器执行环境开始执行。

遇到async1,async2函数分配内存

打印script start

遇到setTimeout,计时完成放到消息队列中,等待主线程空闲时执行。

调用async1函数,打印async1 start 遇到awiait,同步代码,执行后面的async2函数,打印async2(async await是generator的语法糖。可以用generator来实现async await的效果。generator也是微任务队列) 将后面的片段放到微任务队列中。

创建Promise实例对象,打印promise1, 将then函数扔到微任务队列中。

打印script end

主线程空闲, 事件触发线程拿到微任务队列中的第一个任务,放到主线程中的调用栈中执行,打印async1 end

然后执行微任务队列中的第二个微任务,打印promise2,微任务队列清空,去宏任务队列中拿到第一个任务,放到主线程中执行,打印 setTimeout

总结

在面试或者在写代码中,只要明白了这套规则,就了解了js的执行机制。对于一些执行机制以及执行顺序的问题也就迎刃而解了。