虽然大家知道async/await,但是很多人对这个方法中内部怎么执行的还不是很了解
await做了什么处理
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码
,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈后面的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列(Promise的Job Queue)
证明如下:
例子1:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
function async2() {
console.log('async2')
}
async1()
console.log("666")
结果:
例子2:
async function async1() {
console.log('async1 start')
let res = await async2()
console.log(res)
console.log('async1 end')
}
function async2() {
return 1
}
async1()
console.log("666")
结果:
再来难一些的:
例子3:
function testSometing() {
console.log("执行testSometing");
return "testSometing"
}
async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing(); //关键点1
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise((resolve) => {
console.log("promise start..");
resolve("promise");
}); //关键点2
promise.then((val) => console.log(val));
console.log("test end...")
结果:
分析如下:
当test函数执行到
const v1 = await testSometing();
的时候,会先执行testSometing
这个函数打印出“执行testSometing
”的字符串,然后因为await会让出线程就会区执行后面的
var promise = new Promise((resolve)=> {
console.log("promise start..");
resolve("promise");});//关键点2
然后打印出“promise start..”
接下来会把返回的这promise
放入promise
队列(Promise的Job Queue),继续执行打印“test end...”
,等本轮事件循环执行结束后,又会跳回到async函数中(test函数),等待之前await 后面表达式的返回值,因为testSometing
不是async函数,所以返回的是一个字符串“testSometing”
,test函数继续执行,执行到
const v2 = await testAsync();
和之前一样又会跳出test函数,执行后续代码,此时事件循环就到了promise
的队列,执行promise.then((val)=> console.log(val));then
后面的语句,之后和前面一样又跳回到test函数继续执行。
Node.js的Event Loop:有坑
- timers: 执行
setTimeout
和setInterval
的回调 - pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
- idle, prepare: 仅系统内部使用
- poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
- check:
setImmediate
在这里执行 - close callbacks: 一些关闭的回调函数,如:
socket.on('close', ...)
每个阶段都有一个自己的先进先出的队列,只有当这个队列的事件执行完或者达到该阶段的上限时,才会进入下一个阶段。在每次事件循环之间,Node.js都会检查它是否在等待任何一个I/O或者定时器,如果没有的话,程序就关闭退出了。我们的直观感受就是,如果一个Node程序只有同步代码,你在控制台运行完后,他就自己退出了。
上面的这个流程说简单点就是在一个异步流程里,setImmediate会比定时器先执行
,我们写点代码来试试:
setTimeout(() => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
}, 0);
输出:
setImmediate
setTimeout
我们来理一下这个流程:
- 外层是一个
setTimeout
,所以执行他的回调的时候已经在timers
阶段了 - 处理里面的
setTimeout
,因为本次循环的timers
正在执行,所以他的回调其实加到了下个timers
阶段 - 处理里面的
setImmediate
,将它的回调加入check
阶段的队列 - 外层
timers
阶段执行完,进入pending callbacks,idle,
prepare,poll,这几个队列都是空的,所以继续往下 - 到了
check
阶段,发现了setImmediate
的回调,拿出来执行 - 然后是
close callbacks
,队列是空的,跳过 - 又是
timers
阶段,执行我们的console
但是请注意我们上面console.log('setTimeout')
和console.log('setImmediate')
都包在了一个setTimeout
里面,如果直接写在最外层会怎么样呢?代码改写如下:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
经过多次实验,setTimeout
和setImmediate
的输出顺序是不一定的,但是大多数情况,setTimeout
更快。有时setImmediate
会在前面,为啥?
需要告诉大家一件事情,node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn,
1),这在官方文档中有说明。(说到这里顺便提下,HTML 5里面setTimeout最小的时间限制是4ms)。
原理我们都有了,我们来理一下流程:
- 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times
阶段 - 遇到
setImmediate
塞入check
阶段 - 同步代码执行完毕,进入Event Loop
- 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
- 跳过空的阶段,进入
check
阶段,执行setImmediate
回调
通过上述流程的梳理: 我们发现关键就在这个1毫秒,如果同步代码执行时间较长,进入Event Loop
的时候1毫秒已经过了,setTimeout
执行,如果1毫秒还没到,就先执行了setImmediate
。每次我们运行脚本时,机器状态可能不一样,导致运行时有1毫秒的差距,一会儿setTimeout
先执行,一会儿setImmediate
先执行。但是这种情况只会发生在还没进入timers
阶段的时候。像我们第一个例子那样,因为已经在timers
阶段,所以里面的setTimeout
只能等下个循环了,所以setImmediate
肯定先执行。
process.nextTick,Promise,setTimeout,setImmediate的对比:
setTimeout对比setImmediate
,很坑
setTimeout(function(){
console.log('setTimeout')
})
setImmediate(() => console.log('setImmediate'));
setImmediate(() => console.log('setImmediate'));
setTimeout(function(){
console.log('setTimeout')
})
上面已经说了多数情况下输出都是:
setTimeout
setImmediate
如果过了1毫秒setTimeout没加入到timer阶段队列中
,则是
setImmediate
setTimeout
process.nextTick对比Promise
process.nextTick(() => console.log('nextTick'))
let p = new Promise((resolve,reject)=>{
resolve("gfd")
})
p.then(res=>console.log(res))
let p = new Promise((resolve,reject)=>{
resolve("gfd")
})
p.then(res=>console.log(res))
process.nextTick(() => console.log('nextTick'))
输出都是:
nextTick
gfd
总结:
process.nextTick和Promise都是微任务,但是任务优先级process.nextTick 高于 Promise。
process.nextTick,Promise 和 setTimeout,setImmediate对比:
其实就是微任务和宏任务的对比:
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
process.nextTick(() => console.log('nextTick'))
let p = new Promise((resolve, reject) => {
resolve("gfd")
})
p.then(res => console.log(res))
输出:
nextTick
gfd
setTimeout
setImmediate
没有争议,大家都知道的,微任务和宏任务碰到了会放入各自的任务队列中,等主线程把整体代码(算一次宏任务)执行完后,就会优先调出微任务队列中的任务到主线程中执行
总结:
异步事件包括本轮和次轮事件循环
,本轮循环先于次轮循环执行,而Promise.then()是本轮事件循环,而setTimeout和setInterval是次轮事件循环。
本轮循环:
process.nextTick,Promise
次轮循环:
setTimeout,setInteral,setImmediate
两道综合题:
题目一:
process.nextTick(function() {
console.log("nt1");
})
setTimeout(function() {
console.log('st');
}, 0)
new Promise(function(resolve) {
console.log("promise_s");
resolve();
}).then(function(resolve) {
console.log("promise_call");
})
process.nextTick(function() {
console.log("nt2");
})
console.log('end');
输出:
promise_s
end
nt1
nt2
promise_call
st
题目二: 终极boss
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('setTimeout0')
}, 0)
setTimeout(function() {
console.log('setTimeout3')
}, 3)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function() {
console.log('promise3')
})
console.log('script end')
有两个答案: 都是正确的,关键看setTimeout(function() {console.log('setTimeout3')}, 3)
是否及时加入到check阶段的任务队列中去
没有及时加入进来的结果:
script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setImmediate
setTimeout3
及时加入进来的结果:
script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setTimeout3
setImmediate
以上就是关于我对异步任务输出顺序的所有的总结了,希望能帮到困惑的你