什么是線程
由于JavaScript是單線程語言,因此,在一個進(jìn)程上,只能運(yùn)行一個線程,而不能多個線程同時運(yùn)行。也就是說JavaScript不允許多個線程共享內(nèi)存空間。因此,如果有多個線程想同時運(yùn)行,則需采取排隊(duì)的方式,即只有當(dāng)前一個線程執(zhí)行完畢,后一個線程才開始執(zhí)行。
Heap 、Stack、 Queue
堆:對象被分配在一個堆中,用以表示一個內(nèi)存中未被組織的區(qū)域。我們知道,在函數(shù)被調(diào)用之前,JavaScript引擎會對函數(shù)進(jìn)行編譯(詞法分析、語法分析、代碼生成)的工作。當(dāng)完成編譯時會將函數(shù)(這里不限于函數(shù),JavaScript所有皆對象,除了undefined,null)放入堆中,分配內(nèi)存空間,等待執(zhí)行或者調(diào)用。
棧: 當(dāng)函數(shù)調(diào)用時,會形成一個"執(zhí)行棧"。我們看一個簡單的例子。
function bar(b){
return b*2;
}
function foo(a){
return bar(a * 3);
}
console.log(foo(1)); //6
當(dāng)JavaScript引擎在編譯階段,會將foo、bar置于堆中,分配內(nèi)存空間。當(dāng)調(diào)用foo()時,引擎創(chuàng)建了一個執(zhí)行棧,包含了foo函數(shù)的參數(shù)和局部變量。當(dāng)在foo的詞法作用域中調(diào)用bar時,會將bar函數(shù)推入執(zhí)行棧,并置于foo函數(shù)之上,同時包含bar函數(shù)的參數(shù)和局部變量。當(dāng)bar返回時(此例中bar函數(shù)調(diào)用并返回結(jié)果是瞬間完成的),bar函數(shù)出棧。當(dāng)foo函數(shù)返回結(jié)果時,整個執(zhí)行棧就空了。此時,如果任務(wù)隊(duì)列中存在異步任務(wù),則主線程會讀取任務(wù)隊(duì)列中的任務(wù)。待會介紹任務(wù)隊(duì)列。
任務(wù)隊(duì)列
單線程就意味著,所有任務(wù)(線程)需要排隊(duì),前一個任務(wù)結(jié)束,才會執(zhí)行后一個任務(wù)。如果前一個任務(wù)耗時很長,后一個任務(wù)不得不一直等待。
因此,所有任務(wù)可以分為兩種,一種是同步任務(wù),一種是異步任務(wù)。同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,后一個任務(wù)才會執(zhí)行;異步任務(wù)指的是不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列的任務(wù),只有當(dāng)主線程上的所有同步任務(wù)執(zhí)行完畢之后,主線程才會讀取任務(wù)隊(duì)列,開始執(zhí)行異步任務(wù)。
任務(wù)隊(duì)列是一個事件的隊(duì)列(也可以理解成消息的隊(duì)列),IO設(shè)備完成一項(xiàng)任務(wù),就在"任務(wù)隊(duì)列"中添加一個事件,表示相關(guān)的異步任務(wù)可以進(jìn)入"執(zhí)行棧"了。主線程讀取"任務(wù)隊(duì)列",就是讀取里面有哪些事件。
“任務(wù)隊(duì)列”中的事件,除了IO設(shè)備(ajax獲取服務(wù)器數(shù)據(jù))的事件以外,還包括一些用戶產(chǎn)生的事件(mousehover、click、scroll、keyup等)和定時器等。只要在事件中指定了回調(diào)函數(shù),這些事件發(fā)生時就會進(jìn)入“任務(wù)隊(duì)列”,等待主線程讀取。而主線程讀取任務(wù)隊(duì)列中的異步任務(wù),主要就是讀取回調(diào)函數(shù)。
當(dāng)主線程的所有同步任務(wù)執(zhí)行(排隊(duì)執(zhí)行)完畢之后,就會讀取任務(wù)隊(duì)列中的異步任務(wù),將異步任務(wù)推入執(zhí)行棧中執(zhí)行。任務(wù)隊(duì)列是一個先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),即排在前面的事件,優(yōu)先被主線程讀取。如果存在定時器,時間越短的越先進(jìn)入執(zhí)行棧。
因此,可以做一個簡單總結(jié):
- 當(dāng)主線程開始執(zhí)行同步任務(wù)時,會創(chuàng)建一個"執(zhí)行棧",每一個同步任務(wù)排隊(duì)執(zhí)行,只有前一個任務(wù)執(zhí)行完畢,才會執(zhí)行下一個任務(wù)。同時,執(zhí)行棧與汗水的調(diào)用位置有關(guān)。
- 當(dāng)主線程上的所有同步任務(wù)執(zhí)行完畢之后,主線程會讀取任務(wù)隊(duì)列上的異步任務(wù),并將異步任務(wù)推入執(zhí)行棧中開始執(zhí)行。
- 主線程不斷重復(fù)以上2個步驟。
setTimeout
明白了主線程執(zhí)行相關(guān)任務(wù)的思路后,來看看定時器。上面介紹到,定時器是屬于任務(wù)隊(duì)列中的異步任務(wù)。因此會等待“執(zhí)行?!鄙系乃型饺蝿?wù)執(zhí)行完畢之后,主線程計(jì)算定時器的執(zhí)行時間,再將事件推入“執(zhí)行?!薄?匆粋€簡單的例子。
function foo() {
setTimeout(function() {
console.log(1);
}, 0)
console.log(2);
}
function bar() {
setTimeout(function() {
console.log(3);
}, 0);
console.log(4);
}
foo();
bar();
這段函數(shù)的輸出結(jié)果為2, 4, 1, 3。做一個簡單的分析。
foo、bar函數(shù)的內(nèi)部有相同的結(jié)構(gòu),都有一個定時器和console.log()函數(shù)。當(dāng)foo、bar函數(shù)調(diào)用時,會形成一個“執(zhí)行?!?,主線程會先執(zhí)行“執(zhí)行?!敝械耐饺蝿?wù),即console.log(2), console.log(4),而兩個定時器會被推入任務(wù)隊(duì)列中,等待執(zhí)行。當(dāng)主線程上的同步任務(wù)執(zhí)行完畢之后,結(jié)束定時器的等待,將任務(wù)隊(duì)列中的兩個異步任務(wù)推入“執(zhí)行棧”中執(zhí)行,因此輸出的順序?yàn)?, 4, 1, 3。
定時器的第一個參數(shù)是一個函數(shù),第二個參數(shù)是推遲執(zhí)行的毫秒數(shù)。從函數(shù)的定義上看,如果將時間設(shè)定為0,此時應(yīng)該是立即執(zhí)行定時器才對,為什么輸出順序會不同呢?
需要注意的是,setTimeout()只是將回調(diào)函數(shù)插入到“任務(wù)隊(duì)列”中,因此必須等到主線程上的同步任務(wù)全部執(zhí)行完畢,主線程才會執(zhí)行任務(wù)隊(duì)列中的異步任務(wù),并且,setTimeout會等到同步任務(wù)執(zhí)行完畢之后,再等到任務(wù)隊(duì)列中的異步任務(wù)執(zhí)行完畢之后才開始執(zhí)行。setTimeout的第二個參數(shù)只能確保任務(wù)在指定的時間之后執(zhí)行,而不能保證一定就在該時間之后立即執(zhí)行,是否能夠立即執(zhí)行,取決于“執(zhí)行?!敝械娜蝿?wù)數(shù)量。
function foo() {
setTimeout(function() {
console.log(1);
}, 2000)
console.log(2);
}
function bar() {
setTimeout(function() {
console.log(3);
}, 1000);
console.log(4);
}
function baz() {
setTimeout(function() {
console.log(5);
}, 0)
console.log(6);
}
foo();
bar();
baz();
//結(jié)果: 2, 4, 6, 5, 3, 1;
主線程上的同步任務(wù)按照執(zhí)行棧排隊(duì)執(zhí)行,任務(wù)隊(duì)列上的定時器按照時間長短排隊(duì)執(zhí)行。時間越短,越早進(jìn)入“執(zhí)行?!?,越早被主線程執(zhí)行。也就是說,先進(jìn)入任務(wù)隊(duì)列的任務(wù)先執(zhí)行。
如果換一種函數(shù)的調(diào)用位置
baz();
foo();
bar();
//此時的結(jié)果: 6, 2, 4, 5, 3, 1
從上面的兩種運(yùn)行結(jié)果可以看出,
同步任務(wù)取決于函數(shù)的調(diào)用位置,不同的調(diào)用位置,進(jìn)入執(zhí)行棧的位置就不同,主線程執(zhí)行的順序就不同
異步任務(wù)的執(zhí)行與函數(shù)的調(diào)用位置無關(guān),只取決于執(zhí)行棧的任務(wù)數(shù)量,當(dāng)同步任務(wù)執(zhí)行完畢之后,才會開始執(zhí)行異步任務(wù),并且遵循先進(jìn)入任務(wù)隊(duì)列的事件先執(zhí)行的原則。
參考文獻(xiàn):http://www.cnblogs.com/Uncle-Keith/p/6436047.html