对JS事件循环(Event Loop)的一些个人理解
2019-1-7更新
之前对async/await的理解出了一点问题,因此我也下了这篇文章后,再去摸索清楚这个ES2017语法的执行顺序,先看完这两个定义之后再往下看文章(定义来源于阮一峰老师的《ECMA script 6 入门》)
async
函数返回一个 Promise 对象
正常情况下,
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
-------------------------------------------------分割线--------------------------------------------------------------
前言
最近看了一篇讲述了JS事件循环的文章(https://www.jianshu.com/p/12b9f73c5a4f),个人觉得讲得很透彻,看了感觉收益颇多,因此mark下现在的记忆。
基础知识
执行栈:当我们调用一个方法的时候,JS会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为JS是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
任务队列:JS引擎遇到一个异步任务后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,JS会将这个任务加入与当前执行栈不同的另一个队列,我们称之为任务队列
微任务、宏任务:任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
按照我自己的理解,是这样的:
- 代码从宏任务(script整体代码)开始执行
- 如果遇到同步操作,则直接执行;
- 如果遇到异步操作,则将它里面的异步任务分发到对应的任务队列中(可以有多个队列,例如说:promise队列、settimeout队列、process队列等)。
- 接着继续执行直到 执行栈 为空
- 紧接着开始执行微任务中的任务队列(Promise队列、async/await队列等)
- 直到将微任务中所有的任务队列全部执行完毕
- 然后开始又返回去执行宏任务中的任务队列,就这样形成一个环状循环
下面我们通过一道题目来更浅显的理解它
async function a1() {
console.log('async1 start');
await a2();
console.log('async1 end')
}
async function a2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
}, 0);
a1();
new Promise((resolve)=>{
console.log('Promise1');
resolve();
}).then(()=>{
console.log('Promise2')
})
执行结果:
script start
async1 start
async2
Promise1
Promise2
async1 end
setTimeout
无标题.png
(图画得粗糙了点,还是强烈推荐看文章开始的那个链接,里面讲得非常详细)
开始之前
根据文章开头的两段定义:
-
async
函数返回一个 Promise 对象 - 正常情况下,
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
因此await a2()
是可以转化成resolve(Promise.resolve())
的,也就是resolve里面还嵌套着一个resolve。
我们先把async/await转化成promise对象
async function a1() {
console.log('async1 start');
await a2();
console.log('async1 end')
}
async function a2(){
console.log('async2')
}
等于
function a1(){
console.log('async1 start')
new Promise(resolve=>{
console.log('async2')
resolve(Promise.resolve()) <================重点!!
}).then(()=>{
console.log('async1 end')
})
}
虽然我们在这里已经将async转成promise,可以不去管await了,但是我们还是可以了解一下这段话的
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈(后面会详述)的代码。
开始
1、首先从console.log('script start')
开始,因为这是一个同步任务,所以直接在执行栈执行输出了script start
2、遇到一个异步任务分发器setTimeout(setTimeout本身并不是异步,只是里面的回调函数异步而已),它将这个任务分发到宏任务的setTimeout队列中。
3、往下执行遇到a1()
,输出里面的同步任务async1 start
,然后进入a1()
里面的promise
,输出async2
,往下遇到resolve(Promise.resolve())
,将它添加到微任务的Promise队列中,
4、跳出a1,进入下方那个Promise
,输出它里面的同步内容Promise1
,然后将resolve()
添加进Promise队列中,
至此所有的同步任务都执行完了,让我们来看看此时的任务队列情况
1.png5、现在开始执行微任务,当进入第一个第一个resolve的时候发现,它里面还是一个resolve,因此将它继续添加到微任务的Promise队列中(这也就是为什么async会比promise晚输出)
6、现在微任务就只剩下两个没有嵌套的resolve()
了,依次执行,输出Promise2
、
async1 end
7、最后回到宏任务去执行setTimeout
最后
大家可以用上面的思路解决这一道题目:
function a1() { 《=====普通函数
console.log("执行a1");
return "a1";
}
async function a2() { 《=====async函数
console.log("执行a2");
return Promise.resolve("a2");
}
async function test() {
console.log("test start...");
const v1 = await a1();
console.log(v1);
const v2 = await a2();
console.log(v2);
console.log(v1, v2);
}
test();
var promise1 = new Promise((resolve)=> { console.log("promise1 start.."); resolve("promise1");});
promise1.then((val)=> console.log(val));
var promise2 = new Promise((resolve)=> { console.log("promise2 start.."); resolve("promise2");});
promise2.then((val)=> console.log(val));
console.log("test end...")
答案(node 10.14):
test start...
执行a1
promise1 start..
promise2 start..
test end...
a1
执行a2
promise1
promise2
a2
a1 a2
结语
我已经尽量将步骤分得细一些。写这篇文章最主要的目的是做个人记录,如果有哪些出错的地方,希望大家能够指出,我将不胜感激~