Javascript的單線程和異步編程

運行時概念

下面的內容解釋了一個理論上的模型。現代 JavaScript 引擎着重實現和優化了描述的幾個語義。html

可視化描述

Stack, heap, queue

函數調用造成了一個棧幀。前端

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));

 

當調用bar時,建立了第一個幀 ,幀中包含了bar的參數和局部變量。當bar調用foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了foo的參數和局部變量。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。當bar返回的時候,棧就空了。node

對象被分配在一個堆中,即用以表示一個大部分非結構化的內存區域。git

隊列

一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都有一個爲了處理這個消息相關聯的函數。github

事件循環時,runtime (運行時)老是從最早進入隊列的一個消息開始處理隊列中的消息。正因如此,這個消息就會被移出隊列,並將其做爲輸入參數調用與之關聯的函數。爲了使用這個函數,調用一個函數老是會爲其創造一個新的棧幀( stack frame),一如既往。ajax

函數的處理會一直進行直到執行棧再次爲空;而後事件循環(event loop)將會處理隊列中的下一個消息(若是還有的話)。編程

事件循環

之因此稱爲事件循環,是由於它常常被用於相似以下的方式來實現:api

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

若是當前沒有任何消息queue.waitForMessage 會等待同步消息到達。瀏覽器

Javascript的單線程和異步編程

爲何JavaScript是單線程?

JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。網絡

JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

單線程--異步

說到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裏面,等到主程序執行完後再執行。
  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);
    }

    這種方法經過綁定自定義事件,對方法一解耦,這樣能夠經過綁定不一樣的事件,實現不一樣的回調函數,但若是應用這種方法過多,不利於閱讀程序。

  • 發佈/訂閱

    上面"事件",徹底能夠理解成"信號"。

    咱們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作"發佈/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。

    這個模式有多種實現,下面採用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個插件。

    首先,f2向"信號中心"jQuery訂閱"done"信號。

  • jQuery.subscribe("done", f2);

    而後,f1進行以下改寫:

  • function f1(){
    
        setTimeout(function () {
    
          // f1的任務代碼
    
          jQuery.publish("done");
    
        }, 1000);
    
      }

    jQuery.publish("done")的意思是,f1執行完成後,向"信號中心"jQuery發佈"done"信號,從而引起f2的執行。

    此外,f2完成執行後,也能夠取消訂閱(unsubscribe)。

  • jQuery.unsubscribe("done", f2);
  •  

    這種方法的性質與"事件監聽"相似,可是明顯優於後者。由於咱們能夠經過查看"消息中心",瞭解存在多少信號、每一個信號有多少訂閱者,從而監控程序的運行。

  • Nodejs非阻塞式IO/事件驅動IO

異步的好處和適合的場景

  1. 異步的好處
    咱們直接經過一個例子對同步和異步進行對比,假設有四個任務(編號爲1,2,3,4),它們的執行時間都是10ms,其中任務2是任務3的前置任務,任務2須要20ms的響應時間。下面咱們作下對比,你就知道怎麼實現的非阻塞I/O了。
    同步和異步的對比
    圖片來自Soham Kaman的文章
  2. 適合的場景
    能夠看出,當咱們的程序須要大量I/O操做和用戶請求時,js這個具有單線程,異步,事件驅動多種氣質的語言是多麼應景!相比於多線程語言,它沒必要耗費過多的系統開銷,同時也沒必要把精力用於處理多線程管理,相比於同步執行的語言,宿主環境的異步和事件驅動機制又讓它實現了非阻塞I/O,因此你應該知道它適合什麼樣的場景了吧!

參考:

https://www.cnblogs.com/woodyblog/p/6061671.html

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

相關文章
相關標籤/搜索