异步编程这是一个很难用一两句话解释的事情,但千言万语总得有个开头啊! 我们就先从单线程开始讲起。**

为什么JavaScript是单线程?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样不是能提高执行效率吗?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。


单线程中有一种机制:在每次调用JavaScript引擎时,可以随着时间的推移执行你的程序的代码块儿,有些代码块是立即执行,有些代码块是稍后执行,这也称为”事件轮询 (Event Loop)”。

其实就是想告诉大家 JavaScript 引擎对时间是没有感知的,JavaScript代码的执行是 按需执行环境所安排。 -稍后详细讲述

什么叫轮询?

JavaScript引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。

当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。

如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为 “ 事件循环(Event Loop)” 的原因。

这么说你肯定会有些不太理解,不要紧我们从最简单的代码,来分析主线程和调用栈。

  1. function wait(){
  2. //code
  3. }
  4. wait();
  5. function foo(){
  6. //code
  7. }
  8. foo();

当 V8 准备执行这段代码时,会先将全局执行上下文压入到调用栈中,如下图所示:
image.png
然后 V8 便开始在主线程上执行 wait 函数,首先它会创建 wait 函数的执行上下文,并将其压入栈中,那么此时执行栈、主线程的关系如下图所示:
image.png
等 wait 函数执行结束,V8 就会从栈中弹出 wait 函数的执行上下文,此时的效果如下所示:
image.png
foo函数的执行以此类推。等 foo 函数执行结束,V8 就会结束当前的宏任务,调用栈也会被清空,调用栈被清空后状态如下图所示:
image.png
不管如何请记住这句话:第一个宏任务队列中只有一个任务:执行主线程上的JavaScript代码;

稍后跟大家解释什么是为宏任务,微任务。


现在我们来把代码变得复杂点:

  1. function wait(){
  2. //code
  3. }
  4. wait();
  5. function foo(){
  6. //code
  7. }
  8. setTimeout(foo,,100);

为什么说这里复杂,原因是在浏览器中会把I/O、setTimeout、setInterval、requestAnimationFrame 操作分类成宏任务。
在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。

微任务、宏任务都为异步任务。

并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调函数到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

一旦是执行栈中所有的同步任务执行完毕,系统就会读取任务队列。

我们只需记住当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。 在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

宏任务执行完毕后callback放入任务队列。

刚刚的代码我们需要分成两张图来理解:
图1:
image.png
图2:
image.png

宏任务特征:

  1. 宏任务所处的队列就是宏任务队列 第一个宏任务队列中只有一个任务:执行主线程上的JS代码。
  2. 如果遇到I/O、setTimeout、setInterval、requestAnimationFrame异步任务,会创建出一个新的宏任务队列,存放这些异步函数执行完成后的回调函数。
  3. 宏任务队列可以有多个。
  4. 宏任务中可以创建微任务,但是在宏任务中创建的微任务不会影响当前宏任务的执行。
  5. 当一个宏任务队列中的任务全部执行完后,会查看是否有微任务队列,如果有就会优先执行微任务队列中的所有任务,如果没有就查看是否有宏任务队列。

微任务
在浏览器中微任务有Promise.then.catch finally、MutationObserver。
为什么要设计微任务呢?
通过刚刚的学习我们知道宏任务需要先被放到消息队列中,如果某些宏任务的执行时间过久,那么就会影响到消息队列后面的宏任务的执行,而且这个影响是不可控的,因为你无法知道前面的宏任务需要多久才能执行完成。所以宏任务无法胜任一些对精度和实时性要求较高的场景,而微任务可以在实时性和效率之间做有效的权衡。

于是 JavaScript 中又引入了微任务,微任务会在当前的任务快要执行结束时执行,利用微任务,你就能比较精准地控制你的回调函数的执行时机。
微任务其实是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

微任务特征:**

  1. 微任务所处的队列就是微任务队列。
  2. 在上一个宏任务队列执行完毕后,如果有微任务队列就会执行微任务队列中的所有任务。
  3. 微任务队列上创建的微任务,仍会阻碍后方将要执行的宏任务队列 。
  4. 由微任务创建的宏任务,会被丢在异步宏任务队列中执行。