JavaScript的單線程和異步詳細

前言:

說到JavaScript的單線程(single threaded)和異步(asynchronous),很多同學不禁會想,這不是自相矛盾麼?其實,單線程和異步確實不能同時成為一個語言的特性。js選擇瞭成為單線程的語言,所以它本身不可能是異步的,但js的宿主環境(比如瀏覽器,Node)是多線程的,宿主環境通過某種方式(事件驅動,下文會講)使得js具備瞭異步的屬性。往下看,你會發現js的機制是多麼的簡單高效!

瀏覽器:

js是單線程語言,瀏覽器隻分配給js一個主線程,用來執行任務(函數),但一次隻能執行一個任務,這些任務形成一個任務隊列排隊等候執行,但前端的某些任務是非常耗時的,比如網絡請求,定時器和事件監聽,如果讓他們和別的任務一樣,都老老實實的排隊等待執行的話,執行效率會非常的低,甚至導致頁面的假死。所以,瀏覽器為這些耗時任務開辟瞭另外的線程,主要包括http請求線程,瀏覽器定時觸發器,瀏覽器事件觸發線程,這些任務是異步的。下圖說明瞭瀏覽器的主要線程。

一、任務隊列

剛才說到瀏覽器為網絡請求這樣的異步任務單獨開瞭一個線程,那麼問題來瞭,這些異步任務完成後,主線程怎麼知道呢?答案就是回調函數,整個程序是事件驅動的,每個事件都會綁定相應的回調函數,舉個栗子,有段代碼設置瞭一個定時器

setTimeout(function(){
    console.log(time is out);
},50);


執行這段代碼的時候,瀏覽器異步執行計時操作,當50ms到瞭後,會觸發定時事件,這個時候,就會把回調函數放到任務隊列裡。整個程序就是通過這樣的一個個事件驅動起來的。
所以說,js是一直是單線程的,瀏覽器才是實現異步的那個傢夥。

說回主線程:

js一直在做一個工作,就是從任務隊列裡提取任務,放到主線程裡執行。下面我們來進行更深一步的理解。

我們把剛才瞭解的概念和圖中做一個對應,上文中說到的瀏覽器為異步任務單獨開辟的線程可以統一理解為WebAPIs,上文中說到的任務隊列就是callback queue,我們所說的主線程就是有虛線組成的那一部分,堆(heap)和棧(stack)共同組成瞭js主線程,函數的執行就是通過進棧和出棧實現的,比如圖中有一個foo()函數,主線程把它推入棧中,在執行函數體時,發現還需要執行上面的那幾個函數,所以又把這幾個函數推入棧中,等到函數執行完,就讓函數出棧。等到stack清空時,說明一個任務已經執行完瞭,這時就會從callback queue中尋找下一個人任務推入棧中(這個尋找的過程,叫做event loop,因為它總是循環的查找任務隊列裡是否還有任務)。

二、借以解釋幾個容易困惑的問題

1、setTimeout(f1,0)是什麼鬼

這個語句最大的疑問是,f1是不是立刻執行?答案是不一定,因為要看主線程內的命令是否已經執行完瞭,如下代碼:

setTimeout(function(){
console.log(1);
},0);
console.log(2);

2、Ajax請求是否異步

瞭解完上文內容,我們就知道瞭,ajax請求內容的時候是異步的,當請求完成後,會觸發請求完成的事件,然後把回調函數放入callback queue,等到主線程執行該回調函數時還是單線程的。

3、界面渲染線程是單獨開辟的線程

界面渲染線程是單獨開辟的線程,是不是DOM一變化,界面就立刻重新渲染?

如果DOM一變化,界面就立刻重新渲染,效率必然很低,所以瀏覽器的機制規定界面渲染線程和主線程是互斥的,主線程執行任務時,瀏覽器渲染線程處於掛起狀態。

三、如何利用瀏覽器的異步機制

我們已經知道,js一直是單線程執行的,瀏覽器為幾個明顯的耗時任務單獨開辟線程解決耗時問題,但是js除瞭這幾個明顯的耗時問題外,可能我們自己寫的程序裡面也會有耗時的函數,這種情況怎麼處理呢?我們肯定不能自己開辟單獨的線程,但我們可以利用瀏覽器給我們開放的這幾個窗口,瀏覽器定時器線程和事件觸發線程是好利用的,網絡請求線程不適合我們使用。下面我們具體看一下:

假設耗時函數是f1,f1是f2的前置任務。

利用定時器觸發線程:

function f1(callback){
setTimeout(function(){
    // f1 的代碼
    callback();
},0);
}
f1(f2);

這種寫法的耦合度高。

利用事件觸發線程:

$f1.on('custom',f2);  //這裡綁定事件以jQuery寫法為例
function f1(){
setTimeout(function(){
    // f1的代碼
    $f1.trigger('custom');
},0);
}


這種方法通過綁定自定義事件,對方法一解耦,這樣可以通過綁定不同的事件,實現不同的回調函數,但如果應用這種方法過多,不利於閱讀程序。

四、異步的好處和適合的場景

異步的好處:

我們直接通過一個例子對同步和異步進行對比,假設有四個任務(編號為1,2,3,4),它們的執行時間都是10ms,其中任務2是任務3的前置任務,任務2需要20ms的響應時間。下面我們做下對比,你就知道怎麼實現的非阻塞I/O瞭。

適合的場景:

可以看出,當我們的程序需要大量I/O操作和用戶請求時,js這個具備單線程,異步,事件驅動多種氣質的語言是多麼應景!相比於多線程語言,它不必耗費過多的系統開銷,同時也不必把精力用於處理多線程管理,相比於同步執行的語言,宿主環境的異步和事件驅動機制又讓它實現瞭非阻塞I/O,所以你應該知道它適合什麼樣的場景瞭吧!

到此這篇關於JavaScript的單線程和異步詳細的文章就介紹到這瞭,更多相關JavaScript的單線程和異步內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: