Js中process.nextTick,setImmediate,setTimeout,Promise.then,async/await终极异步执行顺序全解析

虽然大家知道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:有坑

在这里插入图片描述

  1. timers: 执行setTimeoutsetInterval的回调
  2. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  3. idle, prepare: 仅系统内部使用
  4. poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  5. check: setImmediate在这里执行
  6. close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

每个阶段都有一个自己的先进先出的队列,只有当这个队列的事件执行完或者达到该阶段的上限时,才会进入下一个阶段。在每次事件循环之间,Node.js都会检查它是否在等待任何一个I/O或者定时器,如果没有的话,程序就关闭退出了。我们的直观感受就是,如果一个Node程序只有同步代码,你在控制台运行完后,他就自己退出了。

上面的这个流程说简单点就是在一个异步流程里,setImmediate会比定时器先执行,我们写点代码来试试:

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

输出:

setImmediate
setTimeout

我们来理一下这个流程:

  1. 外层是一个setTimeout,所以执行他的回调的时候已经在timers阶段了
  2. 处理里面的setTimeout,因为本次循环的timers正在执行,所以他的回调其实加到了下个timers阶段
  3. 处理里面的setImmediate,将它的回调加入check阶段的队列
  4. 外层timers阶段执行完,进入pending callbacks,idle,
    prepare,poll,这几个队列都是空的,所以继续往下
  5. 到了check阶段,发现了setImmediate的回调,拿出来执行
  6. 然后是close callbacks,队列是空的,跳过
  7. 又是timers阶段,执行我们的console

但是请注意我们上面console.log('setTimeout')console.log('setImmediate')都包在了一个setTimeout里面,如果直接写在最外层会怎么样呢?代码改写如下:

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

setImmediate(() => {
    
    
  console.log('setImmediate');
});

经过多次实验,setTimeoutsetImmediate的输出顺序是不一定的,但是大多数情况,setTimeout更快。有时setImmediate会在前面,为啥?

需要告诉大家一件事情,node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn,
1),这在官方文档中有说明。(说到这里顺便提下,HTML 5里面setTimeout最小的时间限制是4ms)。

原理我们都有了,我们来理一下流程:

  1. 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
  2. 遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
  3. 遇到setImmediate塞入check阶段
  4. 同步代码执行完毕,进入Event Loop
  5. 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  6. 跳过空的阶段,进入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

以上就是关于我对异步任务输出顺序的所有的总结了,希望能帮到困惑的你

猜你喜欢

转载自blog.csdn.net/fesfsefgs/article/details/108820069