什么是線程
由于JavaScript是單線程語言,因此速勇,在一個(gè)進(jìn)程上,只能運(yùn)行一個(gè)線程坎拐,而不能多個(gè)線程同時(shí)運(yùn)行烦磁。也就是說JavaScript不允許多個(gè)線程共享內(nèi)存空間养匈。因此,如果有多個(gè)線程想同時(shí)運(yùn)行都伪,則需采取排隊(duì)的方式呕乎,即只有當(dāng)前一個(gè)線程執(zhí)行完畢,后一個(gè)線程才開始執(zhí)行陨晶。
Heap 猬仁、Stack、 Queue
堆:對(duì)象被分配在一個(gè)堆中先誉,用以表示一個(gè)內(nèi)存中未被組織的區(qū)域湿刽。我們知道,在函數(shù)被調(diào)用之前褐耳,JavaScript引擎會(huì)對(duì)函數(shù)進(jìn)行編譯(詞法分析诈闺、語法分析、代碼生成)的工作铃芦。當(dāng)完成編譯時(shí)會(huì)將函數(shù)(這里不限于函數(shù)雅镊,JavaScript所有皆對(duì)象,除了undefined,null)放入堆中刃滓,分配內(nèi)存空間仁烹,等待執(zhí)行或者調(diào)用。
棧: 當(dāng)函數(shù)調(diào)用時(shí)咧虎,會(huì)形成一個(gè)"執(zhí)行棧"卓缰。我們看一個(gè)簡單的例子。
function bar(b){
return b*2;
}
function foo(a){
return bar(a * 3);
}
console.log(foo(1)); //6
當(dāng)JavaScript引擎在編譯階段老客,會(huì)將foo僚饭、bar置于堆中,分配內(nèi)存空間胧砰。當(dāng)調(diào)用foo()時(shí)鳍鸵,引擎創(chuàng)建了一個(gè)執(zhí)行棧,包含了foo函數(shù)的參數(shù)和局部變量尉间。當(dāng)在foo的詞法作用域中調(diào)用bar時(shí)偿乖,會(huì)將bar函數(shù)推入執(zhí)行棧,并置于foo函數(shù)之上哲嘲,同時(shí)包含bar函數(shù)的參數(shù)和局部變量贪薪。當(dāng)bar返回時(shí)(此例中bar函數(shù)調(diào)用并返回結(jié)果是瞬間完成的),bar函數(shù)出棧眠副。當(dāng)foo函數(shù)返回結(jié)果時(shí)画切,整個(gè)執(zhí)行棧就空了。此時(shí)囱怕,如果任務(wù)隊(duì)列中存在異步任務(wù)霍弹,則主線程會(huì)讀取任務(wù)隊(duì)列中的任務(wù)毫别。待會(huì)介紹任務(wù)隊(duì)列。
任務(wù)隊(duì)列
單線程就意味著典格,所有任務(wù)(線程)需要排隊(duì)岛宦,前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)耍缴。如果前一個(gè)任務(wù)耗時(shí)很長砾肺,后一個(gè)任務(wù)不得不一直等待。
因此防嗡,所有任務(wù)可以分為兩種变汪,一種是同步任務(wù),一種是異步任務(wù)本鸣。同步任務(wù)指的是疫衩,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢荣德,后一個(gè)任務(wù)才會(huì)執(zhí)行闷煤;異步任務(wù)指的是不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列的任務(wù)涮瞻,只有當(dāng)主線程上的所有同步任務(wù)執(zhí)行完畢之后鲤拿,主線程才會(huì)讀取任務(wù)隊(duì)列,開始執(zhí)行異步任務(wù)署咽。
任務(wù)隊(duì)列是一個(gè)事件的隊(duì)列(也可以理解成消息的隊(duì)列)近顷,IO設(shè)備完成一項(xiàng)任務(wù),就在"任務(wù)隊(duì)列"中添加一個(gè)事件宁否,表示相關(guān)的異步任務(wù)可以進(jìn)入"執(zhí)行棧"了窒升。主線程讀取"任務(wù)隊(duì)列",就是讀取里面有哪些事件慕匠。
“任務(wù)隊(duì)列”中的事件饱须,除了IO設(shè)備(ajax獲取服務(wù)器數(shù)據(jù))的事件以外,還包括一些用戶產(chǎn)生的事件(mousehover台谊、click蓉媳、scroll、keyup等)和定時(shí)器等锅铅。只要在事件中指定了回調(diào)函數(shù)酪呻,這些事件發(fā)生時(shí)就會(huì)進(jìn)入“任務(wù)隊(duì)列”,等待主線程讀取盐须。而主線程讀取任務(wù)隊(duì)列中的異步任務(wù)玩荠,主要就是讀取回調(diào)函數(shù)。
當(dāng)主線程的所有同步任務(wù)執(zhí)行(排隊(duì)執(zhí)行)完畢之后,就會(huì)讀取任務(wù)隊(duì)列中的異步任務(wù)阶冈,將異步任務(wù)推入執(zhí)行棧中執(zhí)行屉凯。任務(wù)隊(duì)列是一個(gè)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),即排在前面的事件眼溶,優(yōu)先被主線程讀取。如果存在定時(shí)器晓勇,時(shí)間越短的越先進(jìn)入執(zhí)行棧堂飞。
因此,可以做一個(gè)簡單總結(jié):
- 當(dāng)主線程開始執(zhí)行同步任務(wù)時(shí)绑咱,會(huì)創(chuàng)建一個(gè)"執(zhí)行棧"绰筛,每一個(gè)同步任務(wù)排隊(duì)執(zhí)行,只有前一個(gè)任務(wù)執(zhí)行完畢描融,才會(huì)執(zhí)行下一個(gè)任務(wù)。同時(shí),執(zhí)行棧與汗水的調(diào)用位置有關(guān)棚愤。
- 當(dāng)主線程上的所有同步任務(wù)執(zhí)行完畢之后髓涯,主線程會(huì)讀取任務(wù)隊(duì)列上的異步任務(wù),并將異步任務(wù)推入執(zhí)行棧中開始執(zhí)行年叮。
- 主線程不斷重復(fù)以上2個(gè)步驟具被。
setTimeout
明白了主線程執(zhí)行相關(guān)任務(wù)的思路后,來看看定時(shí)器只损。上面介紹到一姿,定時(shí)器是屬于任務(wù)隊(duì)列中的異步任務(wù)。因此會(huì)等待“執(zhí)行椩颈梗”上的所有同步任務(wù)執(zhí)行完畢之后叮叹,主線程計(jì)算定時(shí)器的執(zhí)行時(shí)間,再將事件推入“執(zhí)行棻妫”蛉顽。看一個(gè)簡單的例子终蒂。
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蜂林。做一個(gè)簡單的分析。
foo拇泣、bar函數(shù)的內(nèi)部有相同的結(jié)構(gòu)噪叙,都有一個(gè)定時(shí)器和console.log()函數(shù)。當(dāng)foo霉翔、bar函數(shù)調(diào)用時(shí)睁蕾,會(huì)形成一個(gè)“執(zhí)行棧”,主線程會(huì)先執(zhí)行“執(zhí)行椬涌簦”中的同步任務(wù)瀑凝,即console.log(2), console.log(4),而兩個(gè)定時(shí)器會(huì)被推入任務(wù)隊(duì)列中臭杰,等待執(zhí)行粤咪。當(dāng)主線程上的同步任務(wù)執(zhí)行完畢之后,結(jié)束定時(shí)器的等待渴杆,將任務(wù)隊(duì)列中的兩個(gè)異步任務(wù)推入“執(zhí)行椓戎Γ”中執(zhí)行,因此輸出的順序?yàn)?, 4, 1, 3磁奖。
定時(shí)器的第一個(gè)參數(shù)是一個(gè)函數(shù)囊拜,第二個(gè)參數(shù)是推遲執(zhí)行的毫秒數(shù)。從函數(shù)的定義上看比搭,如果將時(shí)間設(shè)定為0冠跷,此時(shí)應(yīng)該是立即執(zhí)行定時(shí)器才對(duì),為什么輸出順序會(huì)不同呢身诺?
需要注意的是蜜托,setTimeout()只是將回調(diào)函數(shù)插入到“任務(wù)隊(duì)列”中,因此必須等到主線程上的同步任務(wù)全部執(zhí)行完畢霉赡,主線程才會(huì)執(zhí)行任務(wù)隊(duì)列中的異步任務(wù)盗冷,并且,setTimeout會(huì)等到同步任務(wù)執(zhí)行完畢之后同廉,再等到任務(wù)隊(duì)列中的異步任務(wù)執(zhí)行完畢之后才開始執(zhí)行仪糖。setTimeout的第二個(gè)參數(shù)只能確保任務(wù)在指定的時(shí)間之后執(zhí)行,而不能保證一定就在該時(shí)間之后立即執(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ù)按照?qǐng)?zhí)行棧排隊(duì)執(zhí)行故爵,任務(wù)隊(duì)列上的定時(shí)器按照時(shí)間長短排隊(duì)執(zhí)行。時(shí)間越短隅津,越早進(jìn)入“執(zhí)行椢艽梗”,越早被主線程執(zhí)行伦仍。也就是說结窘,先進(jìn)入任務(wù)隊(duì)列的任務(wù)先執(zhí)行。
如果換一種函數(shù)的調(diào)用位置
baz();
foo();
bar();
//此時(shí)的結(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í)行完畢之后,才會(huì)開始執(zhí)行異步任務(wù)卑笨,并且遵循先進(jìn)入任務(wù)隊(duì)列的事件先執(zhí)行的原則孕暇。
參考文獻(xiàn):http://www.cnblogs.com/Uncle-Keith/p/6436047.html