1. 程式人生 > >事件迴圈(Event Loop)之setTimout與Promise

事件迴圈(Event Loop)之setTimout與Promise

這是我今年秋招筆試面試被考頻率最高的一個知識點,沒有之一!在連續摔了兩跤之後,覺得真的有必要把這個知識點整理一下。

1.JavaScript是單執行緒

JavaScript語言的一大特點就是單執行緒,也就是說在同一時間只能做一件事。為了利用多核CPU的計算能力,HTML5提出了Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM,本質上來說並沒有改變JavaScript單執行緒的特性。

2.任務佇列(task queue)

JavaScript單執行緒就意味著所有任務需要排隊,前一個任務結束之後,才能執行下一個任務。

任務可以分為兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務;非同步任務指的是進入“任務佇列”的任務。只有“任務佇列”通知主執行緒某個非同步任務可以執行了,該任務才會進入主執行緒執行。

非同步執行機制:

(1)所有同步任務都在主執行緒上執行,形成一個“執行棧”(execution context stack)

(2)主執行緒之外,還存在一個“任務佇列”。只要非同步任務有了執行結果,就在“任務佇列”中放一個事件

(3)一旦“執行棧”中的所有同步任務執行完畢,系統就會讀取“任務佇列”。找到對應該執行的非同步任務,結束等待狀態,進入執行棧,開始執行。

(4)主執行緒不斷重複上面的第三步。

3.事件迴圈(event loop)

“任務佇列”是一個先進先出的資料結構,主執行緒從“任務佇列”中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop,即事件迴圈。

補充:回撥函式(callback),就是那些會被主執行緒掛起來的程式碼。非同步任務必須指定回撥函式,當主執行緒開始執行非同步任務,就是執行對應的回撥函式。

4.定時器

“任務佇列”中除了放置非同步任務事件以外,還可以放置定時事件,即指定某些程式碼在多少時間之後執行,也稱為“定時器”(timer)功能。定時器主要由setTimeout()和setInterval()這兩個函式來完成,前者指定的程式碼是一次性執行,後者則為反覆執行。

setTimeout(fn,0)的含義是,指定某個任務在主執行緒最早可得的空閒時間執行,它在“任務佇列”的尾部新增一個事件,因此要等到同步任務和“任務佇列”現有的時間都處理完,才會得到執行。要是當前程式碼耗時很長,有可能會等很久,所以並沒有辦法保證回撥函式一定會在setTimeout()指定的時間執行。

5.本輪迴圈和次輪迴圈

非同步任務可以分為兩種:(1)追加在本輪迴圈的非同步任務;(2)追加在次輪迴圈的非同步任務。

上述的迴圈即事件迴圈,Node規定,process.nextTick和Promise的回撥函式,追加在本輪迴圈,即同步任務一旦執行完畢,就開始執行它們。而setTimeout、setInterval、setImmediate的回撥函式,追加在次輪迴圈。

6.微任務(Micro-task)

在一步任務重,process.nextTick執行最快,Promise物件的回撥函式,會進入非同步任務裡面的“微任務”佇列,微任務佇列追加在process.nextTick佇列的後面,也屬於本輪迴圈。

下面來分析一些程式碼。

1.setTimeout

console.log("a");
setTimeout(function(){
	console.log("b");
},0);
console.log("c");
//依次列印 a -> c -> b

解析:執行到setTimeout時,會將其回撥函式放入任務佇列中等候,待主執行緒分別列印a,b後,從任務佇列中讀取並放入主執行緒中執行,因此最後才打印b。

var i=0;
function create(){
	var i=1;
	return function(){
		setTimeout(function(){
			i++;
		},0);
		console.log(i);
	}
}
var print1=create();
print1();
print1();
var print2=create();
print2();
//三個print1()的結果均列印為 1

解析:這段程式碼比較刁鑽,但本質也是考察的非同步執行機制。首先將create()函式賦值給變數print1,然後連續兩次呼叫。create函式的返回值是一個函式,該函式首先設定一個定時器,回撥使得i自增1,會被放入任務佇列中等待,先執行後面的列印語句,即列印區域性變數,也就是create函式中的i,為1。第二次呼叫相當於又一次將i賦值為1,依然是將setTimeout放入任務佇列,先執行列印,也是列印1。單就呼叫print函式來說,setTimeout並沒有起到修改變數的作用,因為每次呼叫,都會重新初始化i的值為1,然後先列印i,再執行定時器事件。

2. Promise

console.log("a");
new Promise(function(resolve){
	console.log("b");
	resolve();
}).then(function(){
	console.log("c");
});
console.log("d");
//依次列印 a -> b -> d -> c
console.log("a");
var p=new Promise(function(resolve){
	console.log("b");
	resolve();
});
console.log("c");
p.then(function(){
	console.log("d");
});
console.log("e");
//依次列印 a -> b -> c -> e -> d

解析:首先會執行同步任務,即列印a,需要注意的是Promise的例項化也屬於同步任務,Promise的非同步體現在then()和catch()中,因此接下來會列印b,Promise呼叫then()方法,其回撥函式會放入任務佇列中等候,待主執行緒的任務執行完畢,再執行then中的回撥函式。

3.setTimeout && Promise

setTimeout(function(){
	console.log("a");
},0);
var p=new Promise(function(resolve,reject){
	console.log("b");
	resolve("c");
});
console.log("d");
p.then(function(value){
	console.log(value);
});
console.log("e");
//按順序依次列印:b -> d -> e -> c -> a

解析:首先依然是先執行同步任務,依次列印b,d,e,然後主執行緒會讀取非同步任務佇列中的事件,因為setTimeout回撥會放到次輪迴圈,而Promise非同步事件會放到本輪迴圈,屬於微任務,優先順序更高,故先執行Promise的非同步,列印c,然後執行setTimeout,列印a。

setTimeout(function(){
	console.log("a");
},0);
new Promise(function(resolve,reject){
	console.log("b");
	resolve();
}).then(function(){
	console.log("c");
});
//依次列印 b -> c -> a
(function test(){
	setTimeout(function(){
		console.log("a");
	},0);
	new Promise(function(resolve){
		console.log("b");
		for(var i=0;i<10000;i++){
			i==9999 && resolve();
		}
		console.log("c");
	}).then(function(){
		console.log("d");
	});
	console.log("e");
})();
//依次列印 b -> c -> e -> d -> a

解析:首先這是一個即時函式,一旦宣告則立即執行。然後需要理解的是Promise內部的邏輯,關於邏輯與的計算規則中有一條是:當第一個運算數為一個布林值true,另一個運算數是物件時,返回這個物件。因此Promise的例項化中的for迴圈,只有當i=9999時才會執行resolve(),且這個回撥會放在then中執行,因此列印b之後會接著列印c,然後繼續同步執行e,最後執行非同步任務,先Promise後setTimeout。

async function async1(){
	console.log("a");
	await async2();
	console.log("b");
}
async function async2(){
	console.log("c");
}
console.log("d");
setTimeout(function(){
	console.log("e");
},0);
async1();
new Promise(function(resolve){
	console.log("f");
	resolve();
}).then(function(){
	console.log("g");
});
console.log("h");
//依次列印 d -> a -> c -> f -> h -> g -> b -> e

解析:這段程式碼中非同步事件不止setTimeout與Promise,還加了一個async。首先需要知道async函式的哪一部分是放在主執行緒中執行,哪一部分放在任務佇列中等候,以及其任務的優先順序。async函式從宣告到執行完await語句的這段程式碼都屬於同步任務,await執行完以後的事件需要放到任務佇列中等候,其優先順序低於Promise,高於setTimeout,也屬於次輪迴圈。因此先執行主執行緒的同步任務,依次列印d,a,c,f,h,之後從任務佇列找到排在前面的任務,即Promise的resolve()回撥,列印g,再執行async1中await後面的語句,列印b,最後執行setTimeout,列印e。