前言
說到j(luò)s的單線程(single threaded)和異步(asynchronous),很多同學(xué)不禁會想积锅,這不是自相矛盾么爽彤?其實(shí),單線程和異步確實(shí)不能同時成為一個語言的特性缚陷。js選擇了成為單線程的語言适篙,所以它本身不可能是異步的,但js的宿主環(huán)境(比如瀏覽器箫爷,Node)是多線程的嚷节,宿主環(huán)境通過某種方式(事件驅(qū)動聂儒,下文會講)使得js具備了異步的屬性。往下看硫痰,你會發(fā)現(xiàn)js的機(jī)制是多么的簡單高效衩婚!
說說瀏覽器
js是單線程語言,瀏覽器只分配給js一個主線程碍论,用來執(zhí)行任務(wù)(函數(shù))谅猾,但一次只能執(zhí)行一個任務(wù),這些任務(wù)形成一個任務(wù)隊(duì)列排隊(duì)等候執(zhí)行鳍悠,但前端的某些任務(wù)是非常耗時的税娜,比如網(wǎng)絡(luò)請求,定時器和事件監(jiān)聽藏研,如果讓他們和別的任務(wù)一樣敬矩,都老老實(shí)實(shí)的排隊(duì)等待執(zhí)行的話,執(zhí)行效率會非常的低蠢挡,甚至導(dǎo)致頁面的假死弧岳。所以,瀏覽器為這些耗時任務(wù)開辟了另外的線程业踏,主要包括http請求線程禽炬,瀏覽器定時觸發(fā)器,瀏覽器事件觸發(fā)線程勤家,這些任務(wù)是異步的腹尖。下圖說明了瀏覽器的主要線程。
再說說任務(wù)隊(duì)列
剛才說到瀏覽器為網(wǎng)絡(luò)請求這樣的異步任務(wù)單獨(dú)開了一個線程伐脖,那么問題來了热幔,這些異步任務(wù)完成后,主線程怎么知道呢讼庇?答案就是回調(diào)函數(shù)绎巨,整個程序是事件驅(qū)動的,每個事件都會綁定相應(yīng)的回調(diào)函數(shù)蠕啄,舉個栗子场勤,有段代碼設(shè)置了一個定時器setTimeout(function(){ console.log(time is out);},50);
執(zhí)行這段代碼的時候歼跟,瀏覽器異步執(zhí)行計時操作和媳,當(dāng)50ms到了后,會觸發(fā)定時事件嘹承,這個時候窗价,就會把回調(diào)函數(shù)放到任務(wù)隊(duì)列里。整個程序就是通過這樣的一個個事件驅(qū)動起來的叹卷。
所以說撼港,js是一直是單線程的坪它,瀏覽器才是實(shí)現(xiàn)異步的那個家伙。
說回主線程
js一直在做一個工作帝牡,就是從任務(wù)隊(duì)列里提取任務(wù)往毡,放到主線程里執(zhí)行。下面我們來進(jìn)行更深一步的理解靶溜。
我們把剛才了解的概念和圖中做一個對應(yīng)开瞭,上文中說到的瀏覽器為異步任務(wù)單獨(dú)開辟的線程可以統(tǒng)一理解為WebAPIs,上文中說到的任務(wù)隊(duì)列就是callback queue罩息,我們所說的主線程就是有虛線組成的那一部分嗤详,堆(heap)和棧(stack)共同組成了js主線程,函數(shù)的執(zhí)行就是通過進(jìn)棧和出棧實(shí)現(xiàn)的瓷炮,比如圖中有一個foo()函數(shù)葱色,主線程把它推入棧中,在執(zhí)行函數(shù)體時娘香,發(fā)現(xiàn)還需要執(zhí)行上面的那幾個函數(shù)苍狰,所以又把這幾個函數(shù)推入棧中,等到函數(shù)執(zhí)行完烘绽,就讓函數(shù)出棧淋昭。等到stack清空時,說明一個任務(wù)已經(jīng)執(zhí)行完了安接,這時就會從callback queue中尋找下一個人任務(wù)推入棧中(這個尋找的過程翔忽,叫做event loop英古,因?yàn)樗偸茄h(huán)的查找任務(wù)隊(duì)列里是否還有任務(wù))妆兑。
借以解釋幾個容易困惑的問題
setTimeout(f1,0)是什么鬼
這個語句最大的疑問是,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后给涕,會立即把匿名函數(shù)放到callback queue里面等待主線程的召喚,但這個時候stack里面并不是空的额获,因?yàn)檫€有一句console.log(2)够庙。等到執(zhí)行完console.log(2)后,才通過event loop把匿名函數(shù)放到stack里面抄邀。所以setTimeout(f1,0)這個語句并不是沒有意義耘眨,如果f1是很耗時的任務(wù),那就應(yīng)該把任務(wù)放到callback queue里面境肾,等到主程序執(zhí)行完后再執(zhí)行剔难。
了解完上文內(nèi)容胆屿,我們就知道了,ajax請求內(nèi)容的時候是異步的偶宫,當(dāng)請求完成后非迹,會觸發(fā)請求完成的事件,然后把回調(diào)函數(shù)放入callback queue纯趋,等到主線程執(zhí)行該回調(diào)函數(shù)時還是單線程的憎兽。
界面渲染線程是單獨(dú)開辟的線程,是不是DOM一變化吵冒,界面就立刻重新渲染纯命?如果DOM一變化,界面就立刻重新渲染痹栖,效率必然很低扎附,所以瀏覽器的機(jī)制規(guī)定界面渲染線程和主線程是互斥的,主線程執(zhí)行任務(wù)時结耀,瀏覽器渲染線程處于掛起狀態(tài)留夜。
如何利用瀏覽器的異步機(jī)制
我們已經(jīng)知道,js一直是單線程執(zhí)行的图甜,瀏覽器為幾個明顯的耗時任務(wù)單獨(dú)開辟線程解決耗時問題碍粥,但是js除了這幾個明顯的耗時問題外,可能我們自己寫的程序里面也會有耗時的函數(shù)黑毅,這種情況怎么處理呢嚼摩?我們肯定不能自己開辟單獨(dú)的線程,但我們可以利用瀏覽器給我們開放的這幾個矿瘦,瀏覽器定時器線程和事件觸發(fā)線程是好利用的枕面,網(wǎng)絡(luò)請求線程不適合我們使用。
異步的好處和適合的場景
1.異步的好處
我們直接通過一個例子對同步和異步進(jìn)行對比缚去,假設(shè)有四個任務(wù)(編號為1,2,3,4)潮秘,它們的執(zhí)行時間都是10ms,其中任務(wù)2是任務(wù)3的前置任務(wù),任務(wù)2需要20ms的響應(yīng)時間易结。下面我們做下對比枕荞,你就知道怎么實(shí)現(xiàn)的非阻塞I/O了。
2.適合的場景
可以看出搞动,當(dāng)我們的程序需要大量I/O操作和用戶請求時躏精,js這個具備單線程,異步鹦肿,事件驅(qū)動多種氣質(zhì)的語言是多么應(yīng)景矗烛!相比于多線程語言,它不必耗費(fèi)過多的系統(tǒng)開銷箩溃,同時也不必把精力用于處理多線程管理瞭吃,相比于同步執(zhí)行的語言碌识,宿主環(huán)境的異步和事件驅(qū)動機(jī)制又讓它實(shí)現(xiàn)了非阻塞I/O,所以你應(yīng)該知道它適合什么樣的場景了吧虱而!