1. 程式人生 > >js的單執行緒和非同步

js的單執行緒和非同步

前言

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

說說瀏覽器

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

圖片來自popAnt 畫得太好,忍不住引過來 (http://blog.csdn.net/kfanning/article/details/5768776

再說說任務佇列

剛才說到瀏覽器為網路請求這樣的非同步任務單獨開了一個執行緒,那麼問題來了,這些非同步任務完成後,主執行緒怎麼知道呢?答案就是回撥函式,整個程式是事件驅動的,每個事件都會繫結相應的回撥函式,舉個栗子,有段程式碼設定了一個定時器

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

執行這段程式碼的時候,瀏覽器非同步執行計時操作,當50ms到了後,會觸發定時事件,這個時候,就會把回撥函式放到任務佇列裡。整個程式就是通過這樣的一個個事件驅動起來的。 所以說,js是一直是單執行緒的,瀏覽器才是實現非同步的那個傢伙。

說回主執行緒

js一直在做一個工作,就是從任務佇列裡提取任務,放到主執行緒裡執行。下面我們來進行更深一步的理解。event loop 圖片來自Philip Roberts的演講《Help, I'm stuck in an event-loop》非常深刻! 我們把剛才瞭解的概念和圖中做一個對應,上文中說到的瀏覽器為非同步任務單獨開闢的執行緒可以統一理解為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,1。因為執行setTimeou後,會立即把匿名函式放到callback queue裡面等待主執行緒的召喚,但這個時候stack裡面並不是空的,因為還有一句console.log(2)。等到執行完console.log(2)後,才通過event loop把匿名函式放到stack裡面。所以setTimeout(f1,0)這個語句並不是沒有意義,如果f1是很耗時的任務,那就應該把任務放到callback queue裡面,等到主程式執行完後再執行。

  1. Ajax請求是否非同步 瞭解完上文內容,我們就知道了,ajax請求內容的時候是非同步的,當請求完成後,會觸發請求完成的事件,然後把回撥函式放入callback queue,等到主執行緒執行該回調函式時還是單執行緒的。
  2. 介面渲染執行緒是單獨開闢的執行緒,是不是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. 非同步的好處 我們直接通過一個例子對同步和非同步進行對比,假設有四個任務(編號為1,2,3,4),它們的執行時間都是10ms,其中任務2是任務3的前置任務,任務2需要20ms的響應時間。下面我們做下對比,你就知道怎麼實現的非阻塞I/O了。同步和非同步的對比 圖片來自Soham Kaman的文章
  2. 適合的場景 可以看出,當我們的程式需要大量I/O操作和使用者請求時,js這個具備單執行緒,非同步,事件驅動多種氣質的語言是多麼應景!相比於多執行緒語言,它不必耗費過多的系統開銷,同時也不必把精力用於處理多執行緒管理,相比於同步執行的語言,宿主環境的非同步和事件驅動機制又讓它實現了非阻塞I/O,所以你應該知道它適合什麼樣的場景了吧!