国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

探秘JS的異步單線程

cooxer / 2370人閱讀

摘要:對(duì)于通常的特別是那些具備并行計(jì)算多線程背景知識(shí)的來講,的異步處理著實(shí)稱得上詭異。而這個(gè)詭異從結(jié)果上講,是由的單線程這個(gè)特性所導(dǎo)致的。的特性之一是單線程,也即是從頭到尾,都在同一根線程下運(yùn)行。而這兩者的不同,便在于單線程和多線程上。

對(duì)于通常的developer(特別是那些具備并行計(jì)算/多線程背景知識(shí)的developer)來講,js的異步處理著實(shí)稱得上詭異。而這個(gè)詭異從結(jié)果上講,是由js的“單線程”這個(gè)特性所導(dǎo)致的。

我曾嘗試用“先定義后展開”的教科書方式去講解這一塊的內(nèi)容,但發(fā)現(xiàn)極其痛苦。因?yàn)橐砬宄@個(gè)東西背后的細(xì)節(jié),并將其泛化、以更高的視角來看問題,著實(shí)涉及非常多的基礎(chǔ)知識(shí)。等到我把這些知識(shí)講清楚、講完,無異于逼迫讀者抱著操作系統(tǒng)、計(jì)算機(jī)網(wǎng)絡(luò)這樣的催眠書看上好個(gè)幾章節(jié),著實(shí)沉悶而乏味。

并且更關(guān)鍵的是,在走到那一步的時(shí)候,讀者的精力早已消耗殆盡,完全沒有心力再去關(guān)心這個(gè)最開始的問題——js的異步處理為何詭異。

所以,我決定反過來,讓我們像一個(gè)初學(xué)者那樣,從一無所知開始,

先使用“錯(cuò)誤的理念”去開始我們的討論,然后用代碼去發(fā)現(xiàn)和理念相違背的地方。

再做出一些修正,再考察一些例子,想想是否還有不大滿意和清楚的地方,再調(diào)整。如此往復(fù),我們會(huì)像偵探那樣,先從一個(gè)不大正確的假設(shè)開始,不斷尋找證據(jù),不斷修正假設(shè),一步步追尋下去,直到抵達(dá)最后完整的真相。

我想,這樣的寫作方式,更符合一個(gè)人真正的求知和研究過程,并能夠?yàn)槟銕砀嚓P(guān)于“探索問題”的啟發(fā)。我想,這樣的思維方式和研究理念,比普通的知識(shí)更為重要。它能夠讓你成為知識(shí)的獵人,有能力獨(dú)立地覓食,而不必被迫成為嬰孩,只能坐等他人喂食。

好了,讓我們先從一塊js代碼,開始我們的探索之旅。

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 5000);

console.log("No. 2");

輸出結(jié)果是:

No. 1
No. 2
setTimeout callback

這塊代碼中幾乎沒什么復(fù)雜的東西,全是打印語句。唯一的特別是函數(shù)setTimeout,根據(jù)粗略的網(wǎng)上資料顯示,它接受兩個(gè)參數(shù):

第一個(gè)參數(shù)是callback函數(shù),就是讓它執(zhí)行完之后,回過頭來調(diào)用的函數(shù)。

另一個(gè)是時(shí)間參數(shù),用于指定多少微妙之后,執(zhí)行callback函數(shù)。這里我們使用了5000微妙,也即是5秒鐘。

另一個(gè)重點(diǎn)是,setTimeout是一個(gè)異步函數(shù),意思是我的主程序不必去等待setTimeout執(zhí)行完畢,將它的運(yùn)行過程扔到別的地方執(zhí)行,然后主程序繼續(xù)往下走。也即是,主程序是一個(gè)步調(diào)、setTimeout是另一個(gè)步調(diào),即是“異步”的方式跑代碼。

如果你有一些并行計(jì)算或者多線程編程的背景知識(shí),那么上面的語句就再熟悉不過了。如果在多線程環(huán)境,無非是另起一根線程去運(yùn)行打印語句console.log("setTimeout callback")。然后主線程繼續(xù)往下走,新線程去負(fù)責(zé)打印語句,清晰明了。

所以綜合起來,這段代碼的意思是,主線程執(zhí)行到語句setTimeout時(shí),就把它交給“其它地方”,讓這個(gè)“其它地方”等待5秒鐘之后運(yùn)行。而主線程繼續(xù)往下走,去執(zhí)行“No. 2”的打印。所以,由于其它部分要等待5秒鐘之后才運(yùn)行,而主線程立刻往下運(yùn)行了“No. 2”的打印,最終的輸出結(jié)果才會(huì)是先打印“No. 2”,再打印“setTimeout callback”。

嗯,so far so good。一切看來都比較美好。

如果我們對(duì)上述程序做一點(diǎn)變動(dòng)呢?例如,我可不可以讓“setTimeout callback”這個(gè)信息先被打印出來呢?因?yàn)樵诓⑿杏?jì)算中,我們經(jīng)常遇到的問題便是,由于你不知道多個(gè)線程之間誰執(zhí)行得快、誰執(zhí)行得慢,所以我們無法判定最終的語句執(zhí)行順序。這里我們讓“setTimeout callback”停留了5秒鐘,時(shí)間太長了,要不短一點(diǎn)?

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 1);

console.log("No. 2");

我們將傳遞給setTimeout的參數(shù)改成了1毫秒。多次運(yùn)行后會(huì)發(fā)現(xiàn),結(jié)果竟然沒有改變?!似乎有點(diǎn)反常,要不再改小一點(diǎn)?改成0?

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 0);

console.log("No. 2");

多次運(yùn)行后,發(fā)現(xiàn)依舊無法改變。這其實(shí)是有點(diǎn)奇怪了。因?yàn)橥ǔ5牟⑿杏?jì)算、多線程編程中,通過多次運(yùn)行,你其實(shí)是可以看到各種無法預(yù)期的結(jié)果的。在這里,竟然神奇地得到了相同的執(zhí)行順序結(jié)果。這就反常了。

但我們還無法完全下一個(gè)肯定的結(jié)論,可不可能因?yàn)槭?b>setTimeout的啟動(dòng)時(shí)間太長,而導(dǎo)致“No. 2”這條語句先被執(zhí)行呢?為了做進(jìn)一步的驗(yàn)證,我們可以在“No. 2”這條打印語句之前,加上一個(gè)for循環(huán),給setTimeout充分的時(shí)間去啟動(dòng)。

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 0);

for (let i = 0; i < 10e8; i++) {}

console.log("No. 2");

運(yùn)行這段代碼,我們發(fā)現(xiàn),"No. 1"這條打印語句很快地顯示到了瀏覽器命令行,等了一秒鐘左右,接著輸出了

No. 2
setTimeout callback

誒?!這不就更加奇怪了嗎?!setTimeout不是等待0秒鐘后立刻運(yùn)行嗎,就算啟動(dòng)再慢,也不至于等待一秒鐘之后,還是無法正常顯示吧?況且,在加入這個(gè)for循環(huán)之前,“setTimeout callback”這條輸出不是立刻就顯示了嗎?

綜合這些現(xiàn)象,我們有理由懷疑,似乎“setTimeout callback”一定是在“No. 2”后顯示的,也即是:setTimeout的callback函數(shù),一定是在console.log("No. 2")之后執(zhí)行的。為了驗(yàn)證它,我們可以做一個(gè)危險(xiǎn)一點(diǎn)的測試,將這個(gè)for循環(huán),更改為無限while循環(huán)。

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 0);

while {}  // dangerouse testing

console.log("No. 2");

如果setTimeout的callback函數(shù)是按照自己的步調(diào)做的運(yùn)行,那么它就有可能在某個(gè)時(shí)刻打印出“setTimeout callback”。而如果真的是按照我們猜測的那樣,“setTimeout callback”必須排在“No. 2”之后,那么瀏覽器命令行就永遠(yuǎn)不會(huì)出現(xiàn)“setTimeout callback”。

運(yùn)行后發(fā)現(xiàn),在瀏覽器近乎要臨近崩潰、達(dá)到內(nèi)存溢出的情形下,“setTimeout callback”依舊沒有打印出來。這也就證明了我們的猜測!

這里,我們第一次出現(xiàn)了理念和現(xiàn)實(shí)的矛盾。按照通常并行計(jì)算的理念,被扔到“其它地方”的setTimeout callback函數(shù),應(yīng)該被同時(shí)運(yùn)行。可事實(shí)卻是,這個(gè)“其它地方”并沒有和后一條打印“No. 2”的語句共同執(zhí)行。這時(shí)候,我們就必須要回到基礎(chǔ),回到j(luò)s這門語言底層的實(shí)現(xiàn)方式上去追查,以此來挖掘清楚這后面的貓膩。

js的特性之一是“單線程”,也即是從頭到尾,js都在同一根線程下運(yùn)行。或許這是一個(gè)值得調(diào)查深入的點(diǎn)。想來,如果是多線程,那么setTimeout也就該按照我們?cè)械睦砟钭鰣?zhí)行了,但事實(shí)卻不是。而這兩者的不同,便在于單線程和多線程上。

找到了這個(gè)不同點(diǎn),我們就可以更深入去思考一些細(xì)節(jié)。細(xì)想起來,所謂“異步”,就是要開辟某個(gè)“別的地方”,讓“別的地方”和你的主運(yùn)行路線一起運(yùn)行。可是,如果現(xiàn)在是單線程,也就意味著計(jì)算資源有且只有一份,請(qǐng)問,你如何做到“同時(shí)運(yùn)行”呢?

這就好比是,如果你去某個(gè)辦事大廳,去繳納水費(fèi)、電費(fèi)、天然氣。那么,我們可以粗略地將它們分為水費(fèi)柜臺(tái)、電費(fèi)柜臺(tái)、天然氣柜臺(tái)。那么,如果我們依次地“先在水費(fèi)柜臺(tái)辦理業(yè)務(wù),等到水費(fèi)的明細(xì)打印完畢、繳納完費(fèi)用后;再跑去電費(fèi)柜臺(tái)打印明細(xì)、加納費(fèi)用;再跑去天然氣柜臺(tái)打印明細(xì)、加納費(fèi)用”,這就是一個(gè)同步過程,必須等待上一個(gè)步驟做完后,才能做下一步。

而異步呢,就是說我們不必在某個(gè)環(huán)節(jié)浪費(fèi)時(shí)間瞎等待。比如,我們可以在“打印水費(fèi)明細(xì)”的空閑時(shí)間,跑到電費(fèi)和天然氣柜臺(tái)去辦理業(yè)務(wù),將“電費(fèi)明細(xì)、天然氣明細(xì)的打印”這兩個(gè)任務(wù)提前啟動(dòng)起來。再回過頭去繳納水費(fèi)、繳納電費(fèi)、繳納天然氣費(fèi)用。其實(shí),這就是華羅庚推廣優(yōu)選法的時(shí)候舉的例子,燒水、倒茶葉、泡茶,如何安排他們的順序?yàn)楦咝А?/p>

顯然,異步地去做任務(wù)更高效。但這要有一個(gè)前提,就是你做任務(wù)的資源,也即是干活的人或者機(jī)器,得有多份才行。同樣按照上面的例子來展開討論,雖然有水費(fèi)、電費(fèi)、天然氣這三個(gè)柜臺(tái),可如果這三個(gè)柜臺(tái)背后的辦事人員其實(shí)只有一個(gè)呢?比如你啟動(dòng)了辦理水費(fèi)的業(yè)務(wù),然后想要在辦理水費(fèi)業(yè)務(wù)的等待期,去電費(fèi)柜臺(tái)辦理電費(fèi)業(yè)務(wù)。表面上,你去電費(fèi)柜臺(tái)下了申請(qǐng)單,請(qǐng)求辦理電費(fèi)業(yè)務(wù),可卻發(fā)現(xiàn)根本沒有辦事員去接收你的這個(gè)業(yè)務(wù)!為何?因?yàn)檫@有且只有一個(gè)的辦事員,還正在辦理你的水費(fèi)業(yè)務(wù)啊!這時(shí)候,你的這個(gè)所謂的“異步”,有何意義?!

所以從這個(gè)角度來看,當(dāng)計(jì)算資源只有一份的時(shí)候,你做“異步”其實(shí)是沒什么意義的。因?yàn)楦苫畹馁Y源只有一份,就算在表面做了名義上的“異步”,可最終就像上面的多柜臺(tái)單一辦事員那樣,到了執(zhí)行任務(wù)層面,還是會(huì)一個(gè)接一個(gè)地完成任務(wù),這就沒有意義了。

那么,js的特性是”單線程“+”異步“,不就正是我們討論的“沒有意義”的情況嗎?!那又為何要多次一舉,干一些沒有意義的事情呢?

嗯......事情變得越來越有趣了。

通常來講,如果一個(gè)事件出現(xiàn)了神奇和怪異的地方,基本上都是因?yàn)槲覀兒雎粤四硞€(gè)細(xì)節(jié),或者對(duì)某個(gè)細(xì)節(jié)存在誤解或是錯(cuò)誤理解。要想把問題解決,我們就必須不斷地回顧已有材料,在不斷地重復(fù)檢驗(yàn)中,發(fā)現(xiàn)那幾根我們忽略的貓膩。

讓我們回顧一下關(guān)于js異步的宣傳片。通常為了說明js異步的必要性,會(huì)舉出瀏覽器的資源加載和頁面渲染這個(gè)矛盾。

渲染,可以比較粗糙地理解為將“畫面”畫出來的過程。例如,瀏覽器要將頁面上的按鈕、圖片顯示出來,就必須有一個(gè)將“圖片”在網(wǎng)頁上畫出來的動(dòng)作。又或是,操作系統(tǒng)要將“桌面”這個(gè)圖形界面顯示在顯示器上,就必須要把它相應(yīng)的這個(gè)“畫面”在顯示器上畫出來的動(dòng)作。歸結(jié)起來,這個(gè)“畫出來”的過程,就被稱之為“渲染”。

例如,你點(diǎn)擊頁面上的一個(gè)button,讓瀏覽器去后端數(shù)據(jù)庫將數(shù)據(jù)報(bào)表取出來,在網(wǎng)頁上把數(shù)字顯示出來。而如果js不支持異步的話,整個(gè)網(wǎng)頁的就會(huì)停留,也即是“卡”,在鼠標(biāo)點(diǎn)擊按鈕這一個(gè)動(dòng)作上,頁面無法完成后續(xù)的渲染工作。一直要等到后端把數(shù)據(jù)返回到了前端,程序流才能夠繼續(xù)跑下去。

所以這里,js的“異步”其實(shí)是為了讓瀏覽器將“加載”這個(gè)任務(wù)分給“其它地方”,讓“加載過程”和“渲染過程”同步進(jìn)行下去。

等等,又是這個(gè)“其它地方”?!!

我擦,不是說js是單線程而么,計(jì)算資源不是只有一份么,怎么又可以“一邊加載、一邊渲染”了?!WTF,你這是在逗我玩兒么?!

艸,到底這里面哪句話是真的?!到底js是單線程是真的?還是說瀏覽器可以同時(shí)做“一邊加載、一邊渲染”這個(gè)事情是真的?!

如何才能解決這個(gè)疑惑?!很顯然,我們必須要深入到瀏覽器的內(nèi)部,去看一看它到底是怎么樣被設(shè)計(jì)的。

在搜索引擎中,做一些關(guān)于瀏覽器和js的搜索,我們不難得到一些基本信息。js并不是瀏覽器的全部,瀏覽器要掌管的事情太多了,掌管js的只是瀏覽器的一個(gè)組件,叫做js引擎。而最出名的、并在Chrome中使用的,就是大名鼎鼎的V8引擎,它負(fù)責(zé)js的解析和運(yùn)行。

另一方面我們還知道,使用js的一個(gè)很大原因,是因?yàn)樗軌蜃杂傻厝ゲ倏谼OM元素、去執(zhí)行Ajax異步請(qǐng)求、能夠像我們最開始舉的例子那樣,使用setTimeout做異步任務(wù)分配。這些都是js優(yōu)秀特性。

可令人驚訝的事情來了,當(dāng)我們?nèi)ヌ剿鬟@個(gè)掌管js一切的V8引擎的時(shí)候,我們卻發(fā)現(xiàn),它并不提供DOM的操控、Ajax的執(zhí)行以及setTimeout的特性:

上圖來自Alexander Zlatkov,它的結(jié)構(gòu)是:

JS引擎

Memory Heap

Call Stack

Web APIs

DOM (Document)

Ajax (XMLHttpRequest)

Timeout (setTimeout)

Callback Queue

Event Loop

明明是js的特性,為什么這些職能卻不是由js的引擎來掌管呢?嗯,interesting~~~

誒!不是“單線程”么,不是加載過程被扔到其它地方么?!js是單線程,也即是js在js引擎中是單線程、只能夠分到一份計(jì)算資源,可是,加載數(shù)據(jù)的Ajax這個(gè)feature不是沒有被放到j(luò)s引擎么?!

艸!真TM是老狐貍啊!還以為“單線程”和“一邊加載、一邊渲染”這兩種說法只有一種是對(duì)的,可結(jié)果是,都對(duì)!為什么呢?因?yàn)橹徽f了js是單線程,可沒說瀏覽器本身是單線程啊!所以咯,渲染相關(guān)的js部分可以和數(shù)據(jù)加載的Ajax部分是可以同時(shí)進(jìn)行的,因?yàn)樗鼈兏揪驮趦蓚€(gè)模塊,即兩個(gè)線程嘛!所以當(dāng)然可以并行啊!WTF!

誒~等等,讓我們?cè)僮屑?xì)看看上面這張圖呢?!Ajax不在js引擎里,可是setTimeout也不在js引擎里面啊!!如果Web APIs這部分是在不同于js引擎的另外一根線程里,它們不就可以實(shí)現(xiàn)真正意義上的并行嗎?!那為何我們開頭的打印信息“setTimeout callback”,無法按照并行的方式,優(yōu)先于“No. 2”打印出來呢?

嗯......真是interesting......事情果然沒有那么簡單。

顯然,我們需要考察更多的細(xì)節(jié),特別是,每一條語句在上圖中,是按照什么順序被移動(dòng)、被執(zhí)行的。

談到語句的執(zhí)行順序,我們需要再一次將關(guān)注點(diǎn)放回到j(luò)s引擎上。再次回看上面這幅結(jié)構(gòu)圖,JS引擎包含了兩部分:一個(gè)是 memory heap,另一個(gè)是call stack。前者關(guān)于內(nèi)存分配,我們可以暫時(shí)放下。后面即是函數(shù)棧,嗯,它就是要進(jìn)一步理解執(zhí)行順序的東西。

函數(shù)棧(call stack)為什么要叫做“棧(stack)”呢?為什么不是叫做函數(shù)隊(duì)列或者別的神馬?這其實(shí)可以從函數(shù)的執(zhí)行順序上做一個(gè)推斷。

函數(shù)最開始被引進(jìn),其實(shí)就是為了代碼復(fù)用和模塊化。我們期望一段本該出現(xiàn)的代碼,被多帶帶提出來,然后只需要用一個(gè)函數(shù)調(diào)用,就可以將這段代碼的執(zhí)行內(nèi)容給插入進(jìn)來。

所以,如果當(dāng)我們執(zhí)行一段代碼時(shí),如果遇到了函數(shù)調(diào)用,我們會(huì)期望先去將函數(shù)里面的內(nèi)容執(zhí)行了,再跳出來回到主程序流,繼續(xù)往下執(zhí)行。

所以,如果把一個(gè)函數(shù)看作函數(shù)節(jié)點(diǎn)的話,整個(gè)執(zhí)行流程其實(shí)是關(guān)于函數(shù)節(jié)點(diǎn)的“深度優(yōu)先”遍歷,也即是從主函數(shù)開始運(yùn)行的函數(shù)調(diào)用,整個(gè)呈深度優(yōu)先遍歷的方式做調(diào)用。而結(jié)合算法和數(shù)據(jù)結(jié)構(gòu)的知識(shí),我們知道,要實(shí)現(xiàn)“深度遍歷”,要么使用遞歸、要么使用stack這種數(shù)據(jù)結(jié)構(gòu)。而后者,無疑更為經(jīng)濟(jì)使用。

所以咯,既然期望函數(shù)調(diào)用呈深度優(yōu)先遍歷,而深度優(yōu)先遍歷又需要stack這種數(shù)據(jù)結(jié)構(gòu)做支持,所以維護(hù)這個(gè)函數(shù)調(diào)用的結(jié)構(gòu)就當(dāng)然呈現(xiàn)為stack的形式。所以叫做函數(shù)棧(stack)。

當(dāng)然,如果再發(fā)散思考一下,操作系統(tǒng)的底層涌來維護(hù)函數(shù)調(diào)用的部分也叫做函數(shù)棧。那為何不用遞歸的方式來實(shí)現(xiàn)維護(hù)呢?其實(shí)很簡單,計(jì)算機(jī)這么個(gè)啥都不懂的東西,如何知道遞歸和返回?它只不過會(huì)一往無前的一直執(zhí)行命令而已。所以,在沒有任何輔助結(jié)構(gòu)的情況下,能夠一往無前地執(zhí)行的方式,只能是stack,而不是更為復(fù)雜的遞歸概念的實(shí)現(xiàn)。

另一方面,回到我們最開頭的問題,矛盾其實(shí)是出現(xiàn)在setTimeout的callback函數(shù)上。而上面的結(jié)構(gòu)圖里,還有一部分叫做“callback queue”。顯然,這一部分也是我們需要了解的東西。

結(jié)合js的call stack和callback queue這兩個(gè)關(guān)鍵詞,我們不難搜索到一些資料,來展開討論這兩部分是如何同具體的語句執(zhí)行相結(jié)合的。

先在整體上論述一下這個(gè)過程:

正常的語句執(zhí)行,會(huì)一條接一條地壓入call stack,執(zhí)行,再根據(jù)執(zhí)行的內(nèi)容繼續(xù)壓入stack。

而如果遇到有Web APIs相關(guān)的語句,則會(huì)將相應(yīng)的執(zhí)行內(nèi)容扔到Web APIs那邊。

Web APIs這邊,可以獨(dú)立于js引擎,并行地分配給它的語句,如Ajax數(shù)據(jù)加載、setTimeout的內(nèi)容。

Web APIs這邊的callback function,會(huì)在在執(zhí)行完相關(guān)語句后,被扔進(jìn)“callback queue”。

Event loop會(huì)不斷地監(jiān)測“call stack”和“callback queue”。當(dāng)“call stack”為空的時(shí)候,event loop會(huì)將“callback queue”里的語句壓入到stack中,繼續(xù)做執(zhí)行。

如此循環(huán)往復(fù)。

以上內(nèi)容比較抽象,讓我們用一個(gè)具體的例子來說明。這個(gè)例子同樣來自于Alexander Zlatkov。使用它的原因很簡單,因?yàn)閆latkov在blog中使用的說明圖,實(shí)在是相當(dāng)清晰明了。而目前我沒有多余的時(shí)間去使用PS繪制相應(yīng)的結(jié)構(gòu)圖,就直接拿來當(dāng)作例子說明了。

讓我們考察下面的代碼片段:

console.log("Hi");
setTimeout(function cb1() { 
    console.log("cb1");
}, 5000);
console.log("Bye");

哈哈,其實(shí)和我們使用的代碼差不多,只是打印的內(nèi)容不同。此時(shí),在運(yùn)行之前,整個(gè)底層的結(jié)構(gòu)是這樣的:

然后,讓我們執(zhí)行第一條語句console.log("Hi"),也即是將它壓入到call stack中:

然后js引擎執(zhí)行stack中最上層的這條語句。相應(yīng)的,瀏覽器的控制臺(tái)就會(huì)打印出信息“Hi”:

由于這條語句被執(zhí)行了,所以它也從stack中消失:

再來壓入第二條語句setTimeout

執(zhí)行setTimeout(function cb1() { console.log("cb1"); }, 5000);

注意,由于setTimout部分并沒有被包含在js引擎中,所以它就直接被扔給了Web APIs的Timeout部分。這里,stack中的藍(lán)色部分起到的作用,就是將相應(yīng)的內(nèi)容“timer、等候時(shí)間5秒、回調(diào)函數(shù)cb1”扔給Web APIs。然后這條語句就可以從stack中消失了:

繼續(xù)壓入下一條語句console.log("Bye")

注意,此時(shí)在Web APIs的部分,正在并行于js引擎執(zhí)行相應(yīng)的語句,即:等候5秒鐘。Okay,timer繼續(xù)它的等待,而stack這邊已經(jīng)有語句了,所以需要把它執(zhí)行掉:

相應(yīng)的瀏覽器控制臺(tái),就會(huì)顯示出“Bye”的信息。而stack中運(yùn)行后的語句,就該消失:

此時(shí),stack已經(jīng)為空。Event loop檢測到stack為空,自然就想要將callback queue中的語句壓入到stack中。可此時(shí),callback queue中也為空,于是Event loop只好繼續(xù)循環(huán)檢測。

另一方面,Web APIs這邊的timer,并行地在5秒鐘后開始了它的執(zhí)行——什么也不做。然后,將它相應(yīng)的回調(diào)函數(shù)cb1(),放到callback queue中:

Event loop由于一直在循環(huán)檢測,此時(shí),看到callback queue有了東西,就迅速將它從callback queue中取出,然后將其壓入到stack里:

現(xiàn)在Stack里有了東西,就需要執(zhí)行回調(diào)函數(shù)cb1()。而cb1()里面調(diào)用了 console.log("cb1")這條語句,所以需要將它壓入stack中:

stack繼續(xù)執(zhí)行,現(xiàn)在它的最上層是 console.log("cb1"),所以需要先執(zhí)行它。于是瀏覽器的控制它打印出相應(yīng)的信息“cb1”:

將執(zhí)行了的 console.log("cb1")語句彈出棧:

繼續(xù)執(zhí)行cb1()剩下的語句。此時(shí),cb1()已經(jīng)沒有其它需要執(zhí)行的語句了,也即是它被運(yùn)行完畢,所以,將它也從stack中彈出:

整個(gè)過程結(jié)束!如果從頭到尾看一遍的話,就是下面這個(gè)gif圖了:

相當(dāng)清晰直觀,對(duì)吧!

如果你想進(jìn)一步地把玩js的語句和call stack、callback queue的關(guān)系,推薦Philip Roberts的一個(gè)GitHub的開源項(xiàng)目:Loupe,里面有他online版本供你做多種嘗試。

有了這些知識(shí),現(xiàn)在我們回過頭去看開頭的那段讓人產(chǎn)生疑惑的代碼:

console.log("No. 1");

setTimeout(function(){
    console.log("setTimeout callback");
}, 0);

console.log("No. 2");

按照上面的js處理語句的順序,第一條語句console.log("No. 1")會(huì)被壓入stack中,然后被執(zhí)行的是setTimout

根據(jù)我們上面的知識(shí),它會(huì)被立刻扔進(jìn)Web APIs中。可是,由于這個(gè)時(shí)候我們給它的等待時(shí)間是0,所以,它的callback函數(shù)console.log("setTimeout callback")會(huì)立刻被扔進(jìn)“Callback Queue”里面。所以,那個(gè)傳說中的“其它地方”指的就是callback queue。

那么,我們能夠期望這一條console.log("setTimeout callback")先于“No. 2”被打印出來嗎?

其實(shí)是不可能的!為什么?因?yàn)橐屗粓?zhí)行,首先它需要被壓入到call stack中。可是,此時(shí)call stack還沒有將程序的主分支上的語句執(zhí)行完畢,即還有console.log("No. 2")這條語句。所以,event loop在stack還未為空的情況下,是不可能把callback queue的語句壓入stack的。所以,最后一條“setTimeout callback”的信息,一定是會(huì)排在“No. 2”這條信息后面被打印出來的!

這完全符合我們之前加入無限while循環(huán)的結(jié)果。因?yàn)橹鞣种б恢北?b>while循環(huán)占有,所以stack就一直不為空,進(jìn)而,callback queue里的打印“setTimeout callback”的語句就更不可能被壓入stack中被執(zhí)行。

探索到這里,似乎該解決的問題也都解決了,好像就可以萬事大吉,直接封筆走人了。可事實(shí)卻是,這才是我們真正的泛化討論的開始!

做研究和探索,如果停留于此,就無異于小時(shí)候自己交作業(yè)給老師,目的僅僅是完成老師布置的任務(wù)。在這里,這個(gè)老師布置的任務(wù)就是文章開頭所提出的讓人疑惑的代碼。可是,解決這段代碼并不是我們的終極目的。我們需要泛化我們的所學(xué)和所知,從更深層次的角度去探索,為什么我們會(huì)疑惑,為什么一開始無法發(fā)現(xiàn)這些潛藏在表面之下不同。我們要繼續(xù)去挖掘,我們到底在哪些最根本的問題上出現(xiàn)了誤解和錯(cuò)誤認(rèn)識(shí),從而導(dǎo)致我們一路如此辛苦,無法在開頭看到事情的真相。

回顧我們的歷程,一開始讓我們載跟斗的,其實(shí)就是對(duì)“異步”和“多線程”的固定假設(shè)。多線程了,就是異步,而異步了,一定是多線程嗎?我們潛意識(shí)里是很想做肯定回答的。這是因?yàn)槿绻惒搅耍珔s是單線程,整個(gè)異步就沒有意義了(回憶那個(gè)多柜臺(tái)、單一辦事員的例子)。可js卻巧妙地運(yùn)用了:使用異步單線程去分配任務(wù),而讓真正做數(shù)據(jù)加載的Ajax、或者時(shí)間等待的setTimeout的工作,扔給瀏覽器的其它線程去做。所以,本質(zhì)上js雖然是單線程的,可在做實(shí)際工作的時(shí)候,卻利用了瀏覽器自身的多線程。這就好比是,雖然是多柜臺(tái)、單一辦事員,可辦事員將繳納電費(fèi)、水費(fèi)的任務(wù),外包給其它公司去做,這樣,雖然自己仍然是一個(gè)辦事員,但卻由于有了外包服務(wù)的支持,依舊可以一起并行來做。

另一方面,js的異步、單線程的特性,逼迫我們?nèi)グ巡⑿杏?jì)算中的“同步/異步、阻塞/非阻塞”等概念理得更清楚。

“同步”的英文是synchronize,但在中文的語境下,卻很容易和“同時(shí)”掛鉤。于是,在潛意識(shí)里有可能會(huì)有這樣一種聯(lián)想,“同步”就是“同時(shí)”,所以,一個(gè)同步(synchronize)的任務(wù)就被理解為“可以一邊做A,一邊做B”。而這個(gè)潛意識(shí)的印象,其實(shí)完全是錯(cuò)誤的(一般做A一邊做B,其實(shí)是“異步”+“并行”的情況)。

但在各類百科詞典上,確實(shí)有用“同時(shí)”來作為對(duì)“同步”的解釋。這是為什么呢?其實(shí)這是對(duì)”同步“用作”同時(shí)“的一個(gè)混淆理解。如果仔細(xì)考慮”同時(shí)“的意思,細(xì)分起來,其實(shí)是有兩種理解:

同一個(gè)時(shí)刻(at the same time),例如在9:00 a.m這個(gè)時(shí)間點(diǎn),我們既在做A也在做B。

另一個(gè)是同一個(gè)時(shí)間參考系,也就是所謂的clock on the wall是同一個(gè)。

前者很容易理解,這里我重點(diǎn)解釋一下后者。例如,我在中國大陸同美國的一個(gè)同學(xué)開微信語音聊天,我這邊是22:00,他那邊是9:00。我們做聊天這件事情的時(shí)候,是同一時(shí)刻(at the same time),但卻不在同一個(gè)時(shí)間參考體系(clock on the wall)。而在計(jì)算機(jī)中討論的同步,其實(shí)討論的是后者的”同一參考系“,同步,就是讓我們的參考系統(tǒng)一起來,放到同一個(gè)體系之下。

又比如,我們?cè)谏钪泻苋菀渍f,同步你的電腦、同步你的手機(jī)通訊錄、同步你的相冊(cè),說的是什么呢?就是讓你的各個(gè)客戶端:PC、手機(jī),同server端服務(wù)器的內(nèi)容都保持一致,也即是大家都被放到一個(gè)一致的參考系里面。不要說,你在PC里有照片A,而在手機(jī)里沒有A卻有B,這個(gè)時(shí)候,談?wù)揚(yáng)C里信息人與談?wù)撌謾C(jī)里信息的人,就是在雞同鴨講。究其原因,就是沒有把大家放到同一個(gè)參考系里面。

所以,同步synchronize所指的”同時(shí)“,是大家把墻上的時(shí)鐘都調(diào)整到一致、調(diào)整為同一個(gè)步調(diào),也即是同時(shí)、同一時(shí)間參考系的意思。而不是說,讓事情在同一時(shí)刻并列發(fā)生。自然的,什么是異步(asynchronize)呢,異步就是大家的時(shí)間參考系是不同的,例如我在中國大陸、你在美國,我們的時(shí)間參考體系是不同的,這就是異步,不在同一個(gè)步調(diào)、頻段上。

事實(shí)上,每一個(gè)獨(dú)立的人、每一塊獨(dú)立的計(jì)算資源,它都代表了一個(gè)各自的參考體系。只要你將任務(wù)分發(fā)給了其他人或是其它計(jì)算資源,此時(shí),就出現(xiàn)了兩個(gè)參考體系:一個(gè)是原有主分支的參考體系,另一個(gè)是新的計(jì)算資源的參考體系。在并行計(jì)算中,有一個(gè)同步機(jī)制是使用語句barrier,目的是讓所有的計(jì)算分支在這一個(gè)位置節(jié)點(diǎn)都完成了計(jì)算。為什么說它是一種同步機(jī)制?按照我們統(tǒng)一參考體系的理解,就是保證其他所有計(jì)算分支完成計(jì)算,也就保證了其它分支的消失,從而只剩下主分支這一個(gè)參考體系。于是大家可以談?wù)撏瑯拥臇|西,說同樣的話,不會(huì)有誤解。

另一方面,如果要更深入地理解js的設(shè)計(jì),我認(rèn)為還需要回到計(jì)算機(jī)歷史的初期,例如那個(gè)只有單核的分時(shí)系統(tǒng)的時(shí)代。在那樣一個(gè)時(shí)代,操作系統(tǒng)所受到的硬件上的限制,不亞于js引擎在瀏覽器中所受到的限制。在同樣的限制下,曾經(jīng)的操作系統(tǒng)會(huì)如何去巧妙運(yùn)用那極為有限的計(jì)算資源,讓整個(gè)操作系統(tǒng)給人以平滑、順暢和功能強(qiáng)大的錯(cuò)覺?我想,js的這些設(shè)計(jì)必定和操作系統(tǒng)早期的設(shè)計(jì)緊密相關(guān)。所以在這個(gè)層面上,它將再一次回到操作系統(tǒng)這樣的基礎(chǔ)知識(shí)上。能否吃透現(xiàn)代的技術(shù),其實(shí)很大層面上取決于你是否吃透了設(shè)計(jì)的歷史,是否理解在那些資源枯竭的年代,各路大神是如何巧妙地逢山開路,遇水搭橋。無論現(xiàn)代的計(jì)算機(jī)硬件資源有多么豐富,它必定會(huì)因?yàn)槟繕?biāo)的主次關(guān)系、業(yè)務(wù)的主次關(guān)系受到限制。而如何在限制中跳舞和創(chuàng)造,這是能夠貫穿整個(gè)歷史的共同性問題。

???

Reference:

How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await

asynchronous vs non-blocking

非同步(Asynchronous)與同步(Synchronous)的差異

Philip Roberts: What the heck is the event loop anyway? | JSConf EU

???

???


近期回顧

《沒有idea這把米,怎么炊熟創(chuàng)業(yè)這碗飯》
《2018年08月寫字總結(jié)》
《財(cái)務(wù)自由所虛構(gòu)的妄念》



如果你喜歡我的文章或分享,請(qǐng)長按下面的二維碼關(guān)注我的微信公眾號(hào),謝謝!




更多信息交流和觀點(diǎn)分享,可加入知識(shí)星球:

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/97572.html

相關(guān)文章

  • Node中事件循環(huán)和異步API

    摘要:異步在中,是在單線程中執(zhí)行的沒錯(cuò),但是內(nèi)部完成工作的另有線程池,使用一個(gè)主進(jìn)程和多個(gè)線程來模擬異步。在事件循環(huán)中,觀察者會(huì)不斷的找到線程池中已經(jīng)完成的請(qǐng)求對(duì)象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會(huì)因阻塞I/O導(dǎo)致硬件資源得不到更優(yōu)的使用。多線程編程也因?yàn)榫幊讨械乃梨i、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)...

    atinosun 評(píng)論0 收藏0
  • 探秘 Node.js 10 新功能背后:V8 release 6.6

    摘要:語言新特性現(xiàn)在返回源代碼的所有內(nèi)容,包括空格和注釋。隨著去年發(fā)布的新的字節(jié)碼解釋器,我們擴(kuò)展了這個(gè)功能,以便在后臺(tái)線程上將源代碼編譯為字節(jié)碼。 每六周,我們都會(huì)創(chuàng)建一個(gè) V8 的新分支,作為我們發(fā)布流程的一部分。每個(gè)版本都是在 Chrome Beta 里程碑之前從 V8 的 Git master 分支出來的。今天(2018-03-27),我們很高興地宣布,我們發(fā)布了一個(gè)新的分支:V8 ...

    springDevBird 評(píng)論0 收藏0
  • 探秘 Node.js 10 新功能背后:V8 release 6.6

    摘要:語言新特性現(xiàn)在返回源代碼的所有內(nèi)容,包括空格和注釋。隨著去年發(fā)布的新的字節(jié)碼解釋器,我們擴(kuò)展了這個(gè)功能,以便在后臺(tái)線程上將源代碼編譯為字節(jié)碼。 每六周,我們都會(huì)創(chuàng)建一個(gè) V8 的新分支,作為我們發(fā)布流程的一部分。每個(gè)版本都是在 Chrome Beta 里程碑之前從 V8 的 Git master 分支出來的。今天(2018-03-27),我們很高興地宣布,我們發(fā)布了一個(gè)新的分支:V8 ...

    Labradors 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<