Js中process.nextTick,setImmediate,setTimeout,Promise.then,async/await終極非同步執行順序全解析

codingWeb發表於2020-09-28

雖然大家知道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

以上就是關於我對非同步任務輸出順序的所有的總結了,希望能幫到困惑的你

相關文章