前言
說到j(luò)s的單線程(single threaded)和異步(asynchronous),很多同學(xué)不禁會(huì)想,這不是自相矛盾么?其實(shí),單線程和異步確實(shí)不能同時(shí)成為一個(gè)語言的特性。js選擇了成為單線程的語言,所以它本身不可能是異步的,但js的宿主環(huán)境(比如瀏覽器,Node)是多線程的,宿主環(huán)境通過某種方式(事件驅(qū)動(dòng),下文會(huì)講)使得js具備了異步的屬性。往下看,你會(huì)發(fā)現(xiàn)js的機(jī)制是多么的簡(jiǎn)單高效!
說說瀏覽器
js是單線程語言,瀏覽器只分配給js一個(gè)主線程,用來執(zhí)行任務(wù)(函數(shù)),但一次只能執(zhí)行一個(gè)任務(wù),這些任務(wù)形成一個(gè)任務(wù)隊(duì)列排隊(duì)等候執(zhí)行,但前端的某些任務(wù)是非常耗時(shí)的,比如網(wǎng)絡(luò)請(qǐng)求,定時(shí)器和事件監(jiān)聽,如果讓他們和別的任務(wù)一樣,都老老實(shí)實(shí)的排隊(duì)等待執(zhí)行的話,執(zhí)行效率會(huì)非常的低,甚至導(dǎo)致頁面的假死。所以,瀏覽器為這些耗時(shí)任務(wù)開辟了另外的線程,主要包括http請(qǐng)求線程,瀏覽器定時(shí)觸發(fā)器,瀏覽器事件觸發(fā)線程,這些任務(wù)是異步的。下圖說明了瀏覽器的主要線程。
再說說任務(wù)隊(duì)列
剛才說到瀏覽器為網(wǎng)絡(luò)請(qǐng)求這樣的異步任務(wù)單獨(dú)開了一個(gè)線程,那么問題來了,這些異步任務(wù)完成后,主線程怎么知道呢?答案就是回調(diào)函數(shù),整個(gè)程序是事件驅(qū)動(dòng)的,每個(gè)事件都會(huì)綁定相應(yīng)的回調(diào)函數(shù),舉個(gè)栗子,有段代碼設(shè)置了一個(gè)定時(shí)器setTimeout(function(){ console.log(time is out);},50);
執(zhí)行這段代碼的時(shí)候,瀏覽器異步執(zhí)行計(jì)時(shí)操作,當(dāng)50ms到了后,會(huì)觸發(fā)定時(shí)事件,這個(gè)時(shí)候,就會(huì)把回調(diào)函數(shù)放到任務(wù)隊(duì)列里。整個(gè)程序就是通過這樣的一個(gè)個(gè)事件驅(qū)動(dòng)起來的。
所以說,js是一直是單線程的,瀏覽器才是實(shí)現(xiàn)異步的那個(gè)家伙。
說回主線程
js一直在做一個(gè)工作,就是從任務(wù)隊(duì)列里提取任務(wù),放到主線程里執(zhí)行。下面我們來進(jìn)行更深一步的理解。
我們把剛才了解的概念和圖中做一個(gè)對(duì)應(yīng),上文中說到的瀏覽器為異步任務(wù)單獨(dú)開辟的線程可以統(tǒng)一理解為WebAPIs,上文中說到的任務(wù)隊(duì)列就是callback queue,我們所說的主線程就是有虛線組成的那一部分,堆(heap)和棧(stack)共同組成了js主線程,函數(shù)的執(zhí)行就是通過進(jìn)棧和出棧實(shí)現(xiàn)的,比如圖中有一個(gè)foo()函數(shù),主線程把它推入棧中,在執(zhí)行函數(shù)體時(shí),發(fā)現(xiàn)還需要執(zhí)行上面的那幾個(gè)函數(shù),所以又把這幾個(gè)函數(shù)推入棧中,等到函數(shù)執(zhí)行完,就讓函數(shù)出棧。等到stack清空時(shí),說明一個(gè)任務(wù)已經(jīng)執(zhí)行完了,這時(shí)就會(huì)從callback queue中尋找下一個(gè)人任務(wù)推入棧中(這個(gè)尋找的過程,叫做event loop,因?yàn)樗偸茄h(huán)的查找任務(wù)隊(duì)列里是否還有任務(wù))。
借以解釋幾個(gè)容易困惑的問題
setTimeout(f1,0)是什么鬼
這個(gè)語句最大的疑問是,f1是不是立刻執(zhí)行?答案是不一定,因?yàn)橐粗骶€程內(nèi)的命令是否已經(jīng)執(zhí)行完了,如下代碼:setTimeout(function(){console.log(1);},0);console.log(2);
這段代碼的輸出結(jié)果是2,1。因?yàn)閳?zhí)行setTimeou后,會(huì)立即把匿名函數(shù)放到callback queue里面等待主線程的召喚,但這個(gè)時(shí)候stack里面并不是空的,因?yàn)檫€有一句console.log(2)。等到執(zhí)行完console.log(2)后,才通過event loop把匿名函數(shù)放到stack里面。所以setTimeout(f1,0)這個(gè)語句并不是沒有意義,如果f1是很耗時(shí)的任務(wù),那就應(yīng)該把任務(wù)放到callback queue里面,等到主程序執(zhí)行完后再執(zhí)行。
了解完上文內(nèi)容,我們就知道了,ajax請(qǐng)求內(nèi)容的時(shí)候是異步的,當(dāng)請(qǐng)求完成后,會(huì)觸發(fā)請(qǐng)求完成的事件,然后把回調(diào)函數(shù)放入callback queue,等到主線程執(zhí)行該回調(diào)函數(shù)時(shí)還是單線程的。
界面渲染線程是單獨(dú)開辟的線程,是不是DOM一變化,界面就立刻重新渲染?如果DOM一變化,界面就立刻重新渲染,效率必然很低,所以瀏覽器的機(jī)制規(guī)定界面渲染線程和主線程是互斥的,主線程執(zhí)行任務(wù)時(shí),瀏覽器渲染線程處于掛起狀態(tài)。
如何利用瀏覽器的異步機(jī)制
我們已經(jīng)知道,js一直是單線程執(zhí)行的,瀏覽器為幾個(gè)明顯的耗時(shí)任務(wù)單獨(dú)開辟線程解決耗時(shí)問題,但是js除了這幾個(gè)明顯的耗時(shí)問題外,可能我們自己寫的程序里面也會(huì)有耗時(shí)的函數(shù),這種情況怎么處理呢?我們肯定不能自己開辟單獨(dú)的線程,但我們可以利用瀏覽器給我們開放的這幾個(gè),瀏覽器定時(shí)器線程和事件觸發(fā)線程是好利用的,網(wǎng)絡(luò)請(qǐng)求線程不適合我們使用。
異步的好處和適合的場(chǎng)景
1.異步的好處
我們直接通過一個(gè)例子對(duì)同步和異步進(jìn)行對(duì)比,假設(shè)有四個(gè)任務(wù)(編號(hào)為1,2,3,4),它們的執(zhí)行時(shí)間都是10ms,其中任務(wù)2是任務(wù)3的前置任務(wù),任務(wù)2需要20ms的響應(yīng)時(shí)間。下面我們做下對(duì)比,你就知道怎么實(shí)現(xiàn)的非阻塞I/O了。
2.適合的場(chǎng)景
可以看出,當(dāng)我們的程序需要大量I/O操作和用戶請(qǐng)求時(shí),js這個(gè)具備單線程,異步,事件驅(qū)動(dòng)多種氣質(zhì)的語言是多么應(yīng)景!相比于多線程語言,它不必耗費(fèi)過多的系統(tǒng)開銷,同時(shí)也不必把精力用于處理多線程管理,相比于同步執(zhí)行的語言,宿主環(huán)境的異步和事件驅(qū)動(dòng)機(jī)制又讓它實(shí)現(xiàn)了非阻塞I/O,所以你應(yīng)該知道它適合什么樣的場(chǎng)景了吧!