前端开发都应该懂的事件循环(event loop)以及异步执行顺序(setTimeout、promise和async/await)

Tip:个人博客主要在CSDN:CSDN博客 ,文章内容有修改的话也在CSDN上,感兴趣的能够去CSDN上关注我~javascript

JS中的事件循环原理以及异步执行过程这些知识点对新手来讲可能有点难,可是是必须迈过的坎,逃避是解决不了问题的,本篇文章旨在帮你完全搞懂它们。html

说明:本篇文章主要是基于浏览器环境,Node环境没有研究过暂时不讨论。文章的内容也是博采众长以及结合本身的理解完成的,相关参考文献文章末尾也会给出,如有侵权请告知整改,或有描述不正确的也欢迎提醒。文章会有点长,耐心看完哦~java

不废话,让咱们从简单到复杂,步步深刻,去享受知识的盛宴~git

1. JS是单线程的

咱们都知道JS是单线程执行的(缘由:咱们不想并行地操做DOM,DOM树不是线程安全的,若是多线程,那会形成冲突),one thread --> one call stack --> one thing at a time,也就是说,它只有一个执行栈(call stack),在同一时刻,JS引擎只能作一件事(JS在执行时有一个很是重要的特性:run to complete,只要运行就直到完成)。看到 “JS是单线程的”这句话的时候,不知道你有没有这样的疑惑:既然JS是单线程的,那么我在网页向后端请求数据的时候,我怎么还能够操做页面:我能够滚动页面,我也能够点击按钮,这不是跟JS是单线程的冲突吗?这个问题困扰了我很久,很大的一个缘由是:我觉得浏览器单单只是由一个JS引擎构成的。以下图(我觉得的浏览器构造,这里以谷歌浏览器chrome为例): github

JS单线程的理解
这里小说明一下:V8是谷歌浏览器的JS执行引擎,在运行JS代码的时候,是以函数做为一个个帧(保存当前函数的执行环境)按代码的执行顺序压入执行栈(call stack)中,栈顶的函数先执行,执行完毕后弹出再执行下一个函数。其中堆是用来存放各类JS对象的。 假设浏览器就是上图的这种结构的话,执行同步代码是没什么问题的,以下 代码1

function foo() {
    bar()
    console.log('foo')
}
function bar() {
    baz()
    console.log('bar')
}
function baz() {
    console.log('baz')
}

foo()
复制代码

咱们定义了foo、bar、baz三个函数,而后调用foo函数,控制台输出的结果为:web

baz
bar
foo
复制代码

执行过程以下:chrome

  1. 一个全局匿名函数最早执行(JS的全局执行入口,以后的例子将忽略),遇到foo函数被调用,将foo函数压入执行栈。
  2. 执行foo函数,发现foo函数体中调用了bar函数,则将bar函数压入执行栈。
  3. 执行bar函数,发现bar函数体中调用了baz函数,又将baz函数压入执行栈。
  4. 执行baz函数,函数体中只有一条语句console.log('baz'),执行,在控制台打印:baz,而后baz函数执行完毕弹出执行栈。
  5. 此时的栈顶为bar函数,bar函数体中的baz()语句已经执行完,接着执行下一条语句(console.log('bar')),在控制台打印:bar,而后bar函数执行完毕弹出执行栈。
  6. 此时的栈顶为foo函数,foo函数体中的bar()语句已经执行完,接着执行下一条语句(console.log('foo')),在控制台打印:foo,而后foo函数执行完毕弹出执行栈。
  7. 至此,执行栈为空,这一轮执行完毕。

仍是图直观点,以上步骤对应的执行流程图以下: segmentfault

执行过程
非动图:
JS单线程的理解(例子图流程)
可是,若是咱们代码中有异步事件该怎么办?

2. 事件循环(event loop)

咱们改变一下代码1代码2以下:后端

function foo() {
    bar()
    console.log('foo')
}
function bar() {
    baz()
    console.log('bar')
}
function baz() { 
    setTimeout(() => {
        console.log('setTimeout: 2s')
    }, 2000)
    console.log('baz') 
}

foo()
复制代码

其余都不变,就在baz函数中增长了一个setTimeout函数。根据1中的假设,浏览器只由一个JS引擎构成的话,那么全部的代码必然同步执行(由于JS执行是单线程的,因此当前栈顶函数无论执行时间须要多久,执行栈中该函数下面的其余函数必须等它执行完弹出后才能执行(这就是代码被阻塞的意思)),执行到baz函数体中的setTimeout时应该等2秒,在控制台中输出setTimeout: 2s,而后再输出:baz。因此咱们指望的输出顺序应该是:setTimeout: 2s -> baz -> bar -> foo这是错的)。api

浏览器若是真这样设计的话,确定是有问题的!!! 遇到AJAX请求、setTimeout等比较耗时的操做时,咱们页面须要长时间等待,就被阻塞住啥也干不了,出现了页面“假死”,这样绝对不是咱们想要的结果。

实际固然并不是我觉得的那样,这里先重点提醒一下:JS是单线程的,这一点也没错,可是浏览器中并不只仅只是由一个JS引擎构成,它还包括其余的一些线程来处理别的事情。以下图(此图参考了Philip Roberts的演讲:《Help, I'm stuck in an event-loop》(YouTube版),被墙的能够看这:《Help, I'm stuck in an event-loop》(bilibili版),这视频推荐你们观看):

ES5浏览器模型
浏览器除了 JS引擎(JS执行线程,后面咱们只关注JS引擎中的执行栈)之外,还有 Web APIs(浏览器提供的接口,这是在JS引擎之外的)线程、 GUI渲染线程等(以下表)。JS引擎在执行过程当中,若是遇到相关的事件(DOM操做、鼠标点击事件、滚轮事件、AJAX请求、setTimeout等),并不会所以阻塞,它会将这些事件移交给Web APIs线程处理,而本身则接着往下执行。Web APIs(这里其实有一个event table,用于记录各类事件)则会按照必定的规则将这些事件放入一个任务队列(callback queue,也叫 task queue, HTML标准定义中,任务队列的数据结构其实不是队列,而是Set(集合),好比,当前执行栈正在执行,即便有一个定时器回调已经在任务队列中等待,此时发生了一个鼠标点击事件,那么该点击事件回调也会添加到任务队列中,此后执行栈变为空,JS引擎是会先取鼠标点击事件的回调执行,而不是先添加到任务队列中的定时器回调。即任务队列中是由一个个集构成的,各个集的执行前后是肯定好的,按集的优先级取回调执行,集內是同一类型的回调才是按照先进先出的队列模式)中,当JS执行栈中的代码执行完毕之后,它就会去任务队列中获取一个事件回调放入执行栈中执行,而后如此往复,这就是所谓的 事件循环机制

线程名 做用
JS引擎线程 也称为JS内核,负责处理JavaScript脚本。(例如V8引擎)
①JS引擎线程负责解析JS脚本,运行代码。
②JS引擎一直等待着任务队列中的任务的到来,而后加以处理。
③一个Tab页(renderer进程)中不管何时都只有一个JS线程运行JS程序。
事件触发线程 归属于渲染进程而不是JS引擎,用来控制事件循环
①当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其余线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中。
②当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
注意:因为JS的单线程关系,因此这些待处理队列中的事件都是排队等待JS引擎处理,JS引擎空闲时才会执行。
定时触发器线程 setInterval和setTimeout所在的线程
①浏览器定时计数器并非由JS引擎计数的。
②JS引擎时单线程的,若是处于阻塞线程状态就会影响计时的准确,所以,经过单独的线程来计时并触发定时。
③计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
异步http请求线程 XMLHttpRequest在链接后经过浏览器新开一个线程请求
将检测到状态变动时,若是设置有回调函数,异步线程就产生状态变动事件,将这个回调放入事件队列中,再由JS引擎执行。
GUI渲染线程 负责渲染浏览器界面,包括:
①解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等。
②重绘(Repaint)以及回流(Reflow)处理。

这里让咱们对事件循环先来作个小总结

  1. JS线程负责处理JS代码,当遇到一些异步操做的时候,则将这些异步事件移交给Web APIs 处理,本身则继续往下执行。
  2. Web APIs线程将接收到的事件按照必定规则按顺序添加到任务队列中(应该是添加到任务集合中的各个事件队列中)。
  3. JS线程处理完当前的全部任务之后(执行栈为空),它会去查看任务队列中是否有等待被处理的事件,如有,则取出一个事件回调放入执行栈中执行。
  4. 而后不断循环第3步。

让咱们来看看真正的浏览器中执行代码1是什么个流程吧! TIP:这里的流程示例网站也是Philip Roberts的演讲中提到的(是他本人写的),能够本身去尝试尝试:传送门

代码1流程动图
代码1中并无出现异步事件,也就不会调用到Web API线程,因此动图中的Web API和任务队列一直为空。 此次,让咱们运行一下有异步事件的 代码2看看什么效果:
代码2执行流程
能够看到,当JS执行栈执行到baz中的setTimeout时,执行栈将该事件推送给Web API处理( Web API开始计时,而不是JS引擎来计时),本身则不被阻塞继续执行,当JS执行栈为空时再去任务队列中获取事件执行。因此代码2的正确运行结果打印出来的顺序应该是: baz -> bar -> foo -> setTimeout: 2s。 细心的小伙伴可能有发现Web API在计时器时间到达后将匿名回调函数添加到任务队列中了,虽然定时器时间已到,但它目前并不能执行!!!由于JS的执行栈此时并不是空,必需要等到当前执行栈为空后才有机会被召回到执行栈执行。由此,咱们能够得出一个结论: setTimeout设置的时间其实只是最小延迟时间,而并非确切的等待时间。(当主线程的任务耗时比较长的时候,等待时间将会变得更长)

相信有了以上的铺垫以后,你对浏览器中JS的执行流程有点感受了,让咱们趁热打铁,进一步探讨事件循环和异步吧~

3. 事件循环(进阶)与异步

3.1 试试setTimeout(fn, 0)

如今让咱们试试0秒延时的setTimeout执行会如何,按道理来讲0秒延迟就是当即执行,那么控制台打印结果应该为:setTimeout: 0s -> foo,事实如此吗?

function foo() {
    console.log('foo');
}
setTimeout(function() {
    console.log('setTimeout: 0s');
}, 0);

foo();
复制代码

实际控制台打印结果的顺序为:foo -> setTimeout: 0s,来看看实际代码执行的过程:

setTimeout0
能够看到,即便setTimeout的延时设置为0(实际上 最小延时 >=4ms,参考 ),JS执行栈也将该延时事件发放给Web API处理,Web API再将事件添加到任务队列中,等JS执行栈为空时,该延时事件再压入执行栈中执行。由此咱们能够得出一个结论: JS执行栈只要遇到异步函数,则无脑推给Web APIs处理。与许多其余语言不一样,JS永不阻塞(也存在一些遗留的意外:如 alert 或者同步 XHR)。 处理 I/O 一般经过 事件回调来执行,因此当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然能够处理其它事情,好比用户输入。

3.2 事件循环中的Promise

如今是时候再深刻一步了,咱们ES6中新增的promise已经火烧眉毛地亮相了!(本篇文章不讨论Promise相关的知识点,若是你对Promise不了解的话,建议先去看看相关知识点)。 其实以上的浏览器模型是ES5标准的,ES6+标准中的任务队列在此基础上新增了一种,变成了以下两种:

  1. 宏任务队列(你们称之为macrotask queue,即callback queue):按HTML标准严格来讲,其实没有macrotask queue这种说法,它也就是ES5中的事件队列,该队列存放的是:DOM事件、AJAX事件、setTimeout事件等的回调。能够经过setTimeout(func)便可将func函数添加到宏任务队列中(使用场景:将计算耗时长的任务切分红小块,以便于浏览器有空处理用户事件,以及显示耗时进度)。
  2. 微任务队列(microtask queue):存放的是Promise事件、nextTick事件(Node.js)等。有一个特殊的函数queueMicrotask(func)能够将func函数添加到微任务队列中。

那么,如今的事件循环模型就变成了以下的样子:

ES6浏览器模型
事件循环的处理流程变成了以下:

  1. JS线程负责处理JS代码,当遇到一些异步操做的时候,则将这些异步事件移交给Web APIs 处理,本身则继续往下执行。
  2. Web APIs线程将接收到的事件按照必定规则添加到任务队列中,宏事件(DOM事件、Ajax事件、setTimeout事件等)添加到宏任务队列中,微事件(Promise、nextTick)添加到微事件队列中
  3. JS线程处理完当前的全部任务之后(执行栈为空),它会先去微任务队列获取事件,并将微任务队列中的全部事件一件件执行完毕,直到微任务队列为空后再去宏任务队列中取出一个事件执行(每次取完一个宏任务队列中的事件执行完毕后,都先检查微任务队列)。
  4. 而后不断循环第3步。

一图胜千言,画个流程图更加清晰,帮助记忆:

ES6事件循环
排一下前后顺序: 执行栈 --> 微任务 --> 渲染 --> 下一个宏任务

3.2.1 单独使用Promise

先来个只有Promise的例子热热身:

function foo() {
    console.log('foo')
}

console.log('global start')

new Promise((resolve) => {
    console.log('promise')
    resolve()
}).then(() => {
    console.log('promise then')
})

foo()

console.log('global end')
复制代码

控制台输出的结果为:

global start
promise
foo
global end
promise then
复制代码

代码执行的解释:

  1. 执行console.log('global start')语句,打印出:global start
  2. 继续往下执行,遇到new Promise(....),执行之(这里说明一点:在使用new关键字来建立Promise对象时,传递给Promise的函数称为executor,当promise被建立的时候executor函数会自动执行,而then里面的东西才是异步执行的部分),Promise参数中的匿名函数与主线程同步执行,执行console.log('promise')打印出:promise。在执行resolve()以后Promise状态变为resolved,再继续执行then(...),遇到then则将其提交给Web API处理,Web API将其添加到微任务队列(注意:此时微任务队列中已有一个Promise事件待处理)。
  3. 执行栈在转交完Promise事件后,继续往下执行,到达语句foo(),执行foo函数,打印出:foo
  4. 执行栈继续执行,到达语句console.log('global end'),执行后打印出:global end。至此,本轮事件循环已结束,执行栈为空。
  5. 事件循环机制首先查看微任务队列是否为空,发现有一个Promise事件待执行,则将其压入执行栈,执行then中的代码,执行console.log('promise then'),打印出:promise then。至此,新的一轮事件循环(Promise事件)已结束,执行栈为空。(注意:此时微任务队列为空
  6. 执行栈变空后又先查看微任务队列,发现微任务队列已为空,而后再查看宏任务队列,发现宏任务队列也为空,那么执行栈进入等待事件状态。

用动图来展现一下执行的流程(备注:该demo网站并未画出微任务队列,咱们需本身脑补一下microtask queue):

promise1

3.2.2 Promise结合setTimeout

咱们已经对单独的宏任务和微任务的执行流程分别作了分析,如今让咱们混合这两种任务的事件来看看结果如何,来个代码示例小试牛刀:

function foo() {
    console.log('foo')
}

console.log('global start')

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

new Promise((resolve) => {
    console.log('promise')
    resolve()
}).then(() => {
    console.log('promise then')
})

foo()

console.log('global end')
复制代码

控制台输出的结果为:

global start
promise
foo
global end
promise then
setTimeout: 0S
复制代码

代码执行的解释:

  1. 执行console.log('global start')语句,打印出:global start
  2. 继续往下执行,遇到setTimeout,JS执行栈将其移交给Web API处理。 延迟0秒后,Web API将setTimeout事件添加到宏任务队列(注意:此时宏任务队列中已有一个setTimeout事件待处理)。
  3. JS线程转交setTimeout事件后本身则继续往下执行,遇到new Promise(....),执行之,Promise参数中的匿名函数同步执行,执行console.log('promise')打印出:promise。在执行resolve()以后Promise状态变为resolved,再继续执行then(...),遇到then则将其提交给Web API处理,Web API将其添加到微任务队列(注意:此时微任务队列中已有一个Promise事件待处理)。
  4. 执行栈在转交完Promise事件后,继续往下执行,到达语句foo(),执行foo函数,打印出foo
  5. 执行栈继续执行,到达语句console.log('global end'),执行后打印出:global end。至此,本轮事件循环已结束,执行栈为空。
  6. 事件循环机制首先查看微任务队列是否为空,发现有一个Promise事件待执行,则将其压入执行栈,执行then中的代码,执行console.log('promise then'),打印出:promise then。至此,新的一轮事件循环(Promise事件)已结束,执行栈为空。(注意:此时微任务队列为空
  7. 执行栈变空后又先查看微任务队列,发现微任务队列已为空,而后再查看宏任务队列,发现有一setTimeout事件待处理,则将setTimeout中的匿名函数压入执行栈中执行,执行console.log('setTimeout: 0s')语句,打印出:setTimeout: 0s。至此,新的一轮事件循环(setTimeout事件)已结束,执行栈为空。(注意:此时微任务队列为空,宏任务队列也为空
  8. 执行栈变空后又先查看微任务队列,发现微任务队列已为空,而后再查看宏任务队列,发现宏任务队列也为空,那么执行栈进入等待事件状态。

这个例子比较详细地解释了一遍,一共发生了三次事件循环。同理,仍是用个动图来直观地展现代码执行过程吧!

promise2
相信耐心看到这的你已经对事件循环机制以及宏任务和微任务的执行顺序有个清晰的了解了吧!不过,还没结束哦,咱们async/await(不了解的人建议先去补习一下: async_function)还没讲呢!

3.3 事件循环中的async/await

这里简单介绍下async函数:

  • 函数前面async关键字的做用就2点:①这个函数老是返回一个promise。②容许函数内使用await关键字。
  • 关键字await使async函数一直等待(执行栈固然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await只在async函数函数里面奏效。
  • async函数只是一种比promise更优雅得获取promise结果(promise链式调用时)的一种语法而已。

像上面同样,咱们先单独拎出async函数来看看是怎么样个执行流程吧~

function foo() {
    console.log('foo')
}

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2() {
    console.log('async2')
}

console.log('global start')
async1()
foo()
console.log('global end')
复制代码

这里就增长了两个async函数:async一、async2。执行的结果以下:

global start
async1 start
async2
foo
global end
async1 end
复制代码

咱们再来逐条解析一下代码的执行过程吧(前面那些咱们已经懂的就不重复了):

  1. 首先执行console.log('global start'),打印出:global start
  2. 执行async1(),进入到async1函数体内,执行console.log('async1 start'),打印出:async1 start。接着执行await async2(),这里await关键字的做用就是await下面的代码只有当await后面的promise返回结果后才能够执行(此时,微任务队列有一事件,其实就是Promise事件),而await async2()语句就像执行普通函数同样执行async2(),进入到async2函数体中;执行console.log('async2'),打印出:async2。async2函数执行结束弹出执行栈。
  3. 由于await关键字以后的语句已经被暂停,那么async1函数执行结束,弹出执行栈。JS主线程继续向下执行,执行foo()函数打印出:foo
  4. 执行console.log('global end'),打印出:global end。该语句以后再无其余需执行的代码,执行栈为空,则本轮事件执行结束。
  5. 此时,事件循环机制开始工做:同理,先查看微任务队列,执行完全部已存在的微任务事件后再去查看宏任务队列。目前微任务队列中的事件即为async1函数中await async2()语句,async2函数执行完毕后,promise状态变为settled,以后的代码就能够继续执行了(能够这么理解:用一个匿名函数包裹await语句以后的代码做为一个微任务事件),执行console.log('async1 end')语句,打印出:async1 end。执行栈又为空,本轮事件也执行结束。
  6. 事件循环机制再查看微任务队列,发现为空,再去查看宏任务队列,发现也为空,则进入等待事件状态。

至此,单一事件类型咱们都掌握了,下面咱们综合演练一下!

4. 大综合(自测)

这里来几道常见的题目来考察本身的掌握程度以及进一步巩固吧!这里再也不逐步分析了,有困惑的能够留言再解答。

4.1 简单融合

//请写出输出内容
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');
复制代码

输出结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
复制代码

4.2 变形1

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2作出以下更改:
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
}

console.log('script start');

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

async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
复制代码

输出的结果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
复制代码

4.3 变形2

async function async1() {
    console.log('async1 start');
    await async2();
    //更改以下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改以下:
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');
复制代码

输出的结果:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
复制代码

4.4 变形3

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

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

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')
复制代码

输出的结果:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
复制代码

怎么样,都作对了吗?其实就是将这几个异步事件糅合在一块儿罢了,只要咱们分别掌握了它们的执行过程,一步步拆开分析,一点都不难,这都是纸老虎而已!

5. 结语

呼~长吁一口气,相信看到这的你已经疲惫了,不过恭喜:你应该彻底掌握了事件循环以及异步执行机制了吧!最后,让咱们再总结一下本文涉及的要点吧!

  1. JS是单线程执行的,同一时间只能处理一件事。可是浏览器是有多个线程的,JS引擎经过分发这些耗时的异步事件(AJAX请求、DOM操做等)给Wep APIs线程处理,所以避免了单线程被耗时的异步事件阻塞的问题。

  2. Web APIs线程会将接收到的全部事件中已完成的事件根据类别分别将它们添加到相应的任务队列中。其中任务队列分如下两种:

    • 宏任务队列(macrotask queue):实际上是叫任务队列,ES5称task queue,也即本文图中的callback queue,macrotask是咱们给它的别名,缘由只是为了与ES6新增的microtask队列做区分而这样称呼,HTML标准中并无macrotask这种说法。它存放的是DOM事件、AJAX事件、setTimeout事件等。
    • 微任务队列(microtask queue):它存放的是Promise事件、nextTick事件等。优先级比macrotask高。
  3. 事件循环(event loop) 机制是为了协调事件(events)、用户交互(user interaction)、JS脚本(scripts)、页面渲染(rendering)、网络请求(networking)等等事件的有序执行而设置(定义由HTML标准给出,实现方式是靠各个浏览器厂商本身实现)。事件循环的过程以下:

    • JS引擎执行一个事件,当遇到异步事件时则将其交给浏览器的Web APIs线程处理,而后该事件继续执行,永远不会被抢占,一直执行到该事件结束为止(run to complete)。
    • 当JS引擎执行完当前事件(即执行栈变为空)以后,它会先去查看microtask队列,将microtask队列中的全部待执行事件所有执行完毕。
    • 等微任务事件所有执行完毕后,再进行页面的渲染,此时代表一轮事件循环的过程结束。而后再去查看macrotask队列,取出一个宏事件添加到执行栈执行,开始一轮新的事件,执行完毕后再去执行全部微任务事件...如此往复。此即事件循环的执行过程。

    打个比方帮助理解:宏任务事件就像是普通用户,而微任务事件就像是VIP用户,执行栈要先把全部在等待的VIP用户服务好了之后才能给在等待的普通用户服务,并且每次服务完一个普通用户之后都要先看看有没有VIP用户在等待,如有,则VIP用户优先(PS:人民币玩家真的能够随心所欲,hah...)。固然,执行栈正在给一个普通用户服务的时候,这时即便来了VIP用户,他也是须要等待执行栈服务完该普通用户后才能轮到他。

  4. setTimeout设置的时间其实只是最小延迟时间,并非确切的等待时间。实际上最小延时 >=4ms,小于4ms的会被当作4ms。

  5. promise 对象是由关键字 new 及Promise构造函数来建立的。该构造函数会把一个叫作“处理器函数”(executor function)的函数做为它的参数(即 new Promise(...)中的...的内容)。这个“处理器函数”是在promise建立时是自动执行的,.then以后的内容才是异步内容,会交给Web APIs处理,而后被添加到微任务队列

  6. async/awaitasync函数实际上是Generator函数的语法糖(解释一下“语法糖”:就是添加标准之外的语法以方便开发人员使用,本质上仍是基于已有标准提供的语法进行封装来实现的),async function 声明用于定义一个返回 AsyncFunction 对象的异步函数。执行async函数时,遇到await关键字时,await 语句产生一个promise,await 语句以后的代码被暂停执行,等promise有结果(状态变为settled)之后再接着执行。

呼~ 结束了,休息一下~

参考文献:

相关文章
相关标签/搜索