1、默认的理论基础
执行上下文(Execution context)
函数调用栈(call stack)
队列数据结构(queue)
Promise
async/await
2、比较难懂的部分基础知识回顾
(1)async/await与Promise的相互转化(这里主要关注的是await(thenable)这种情况下是如何转化为Promise的)
async/await可视为 Promise 的语法糖,两者可以相互转化为彼此的写法:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1();
转化为Promise的写法:
function async1(){
console.log('async1 start');
const p = async2();
return new Promise((resolve) => {
Promise.resolve().then(() => {
p.then(resolve)
})
})
.then(() => {
console.log('async1 end')
});
}
function async2(){
console.log('async2');
return Promise.resolve();
}
async1();
这里面主要是await v的处理有点出乎意料,可能暂时你对上面的转化有点不太理解,不要紧,待我慢慢道来~~
首先一个通用的转化步骤是:
async function async1() {
await fn()
}
这里的fn有可能返回的是Promise(async默认返回的就是Promise),也可能直接返回的结果(return 'something'),但是不论如何,上面的这小段代码都可以转化为:
async function async1() {
return new Promise((resolve) => {resolve(fn())})
}
若await后面还有别的需要执行的语句:
async function async1() {
await fn()
console.log('async1 end')
}
await v 后续的代码的执行类似于传入then()中的回调:
function async1(){
const p = async2();
return new Promise((resolve) => {
resolve(fn())
})
.then(() => {
console.log('async1 end')
});
}
若fn是thenable对象,那么就还可以进一步转化为:
function async1(){
const p = async2();
return new Promise((resolve) => {
Promise.resolve().then(() => {
p.then(resolve)
})
})
.then(() => {
console.log('async1 end')
});
}
我相信到这,应该能够理解文章开始的那段代码的转化结果了吧,如果还是不能够理解,那么可以留言,我一定改到你明白。
3、任务队列
这部分网上也有很多相关的文章,有的讲的也很好,我这边就尽量把我觉得最有助于理解的部分内容说明一下:
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个
任务队列又分为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新特性)
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
上面这些点基本上包含了分析事件循环时所需的基本知识,下面再先给出事件循环的一个过程:
<u>从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。</u>
4、小栗子(一步步的执行顺序)
请看下面一段代码
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
这段代码可能很多人能够给出打印的结果,但是对于其中的执行顺序是否了然于心?下面我们一步步的从任务队列、函数调用栈的角度出发,来分析这段代码:
经过上面这些步骤讲解,相信大家对于事件循环有了一定的认识,当然上面的这段代码是比较简单的,只是为了让大家快速的理解事件循环整个的一个机制,下面有一个综合的小测验希望大家都可以轻易的给出正确的答案。
5、终极考验
setTimeout(function() {
console.log('timeout1');
})
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
async1();
Promise.resolve(
new Promise(function(resolve) {
console.log('qqq');
resolve();
}).then(function(){
console.log('www');
})
)
.then(()=>{
console.log('promise resolve')
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
new Promise(function(resolve) {
console.log('ddd');
resolve();
}).then(function(){
console.log('dcdcd');
})
console.log('promise2');
}).then(function() {
console.log('then1');
}).then(function() {
console.log('then2');
})
console.log('global1');
大家不要对这段程序有什么恐惧心理,就按照此前的分析步骤一步步的去剖解它,就可以得出正确的答案。这里需要提醒的是async1 end的输出位置,请大家留意。
当然如果大家还是感觉一步步的分析有什么困难的话,可以给我留言,我会补一个分析的过程,这里就不赘述了,因为比较长。。
谢谢大家。