試圖探尋JavaScript的異步設計

閱讀本文,你將知道:html

  1. 同步和異步
  2. 阻塞和非阻塞
  3. JavaScript的異步實現方式
  4. 進一步深刻理解爲何「JavaScript是異步事件驅動的單線程編程語言」

前言

在最初學習JavaScript的時候,就從各個地方得知JavaScript是一門單線程編程語言,可是使人疑惑的是,爲何一個單線程語言可以同時執行HTTP請求同時渲染頁面?爲何代碼的書寫順序和執行順序並不一致?web

這就是異步帶來的效果ajax

這裏咱們必須達到以下的共識:編程

  • 瀏覽器是多線程的,常駐線程有:
    • 瀏覽器 GUI 渲染線程
    • JavaScript 引擎線程
    • 瀏覽器定時觸發器線程
    • 瀏覽器事件觸發線程
    • 瀏覽器 http 異步請求線程
  • JavaScript是單線程的

這裏須要注意的是GUI渲染進程和JavaScript引擎進程是互斥的,由於若是這兩個線程能夠同時運行的話,JavaScript的DOM操做將會擾亂渲染線程執行渲染先後的數據一致性。瀏覽器

本文想要探討的,是JavaScript線程裏的異步設計,千萬別和多線程混淆了。網絡

想要了解更多關於瀏覽器多線程機制請參考:www.cnblogs.com/hksac/p/659…多線程

開始

一切得先從CPU開始講起:異步

CPU的指令執行速度是遠高於硬盤讀取速度和主存讀取速度的。而I/O操做就會涉及到硬盤存取和主存讀取,常見的I/O操做有文件I/O,網絡I/O。(I/O = Input / Output)。編程語言

因此,觀察如下這一段僞代碼:函數

var a = 2;

for(let i =0;i<10;i++){
    doSomeWork();
}

let buffer = openFile('./work.txt')

buffer.add('hello world');

複製代碼

在CPU眼中,他會把代碼當作這兩部分:

綠色部分由於不涉及到I/O操做,因此CPU執行速度超快,可是當運行到紅色部分時,倒是一個很是耗時的操做,而這段時間,CPU是處於一個'無所事事'的狀態(DMA獲取總線控制權以後一切I/O與CPU無關),由於文件若是沒有讀取進來,下面的工做也沒法開展。

同步在這裏的意思,即書寫代碼的順序就是代碼執行的順序,若是JavaScript設計成同步的話,那麼當執行到openFile這一行的時候,將會等待該I/O操做完成CPU才繼續往下執行。

設想一下,當發送Ajax請求(網絡I/O)的時候,整個頁面被阻塞沒法操做將會是多差的體驗。

而諸如鼠標點擊事件,滑動事件,失焦事件,在CPU看來,都是處理得特別慢的事件(雖然對咱們來講是一瞬間的事情),若是將JavaScript設計成同步,也會特別浪費CPU性能。

阻塞和非阻塞關注的CPU在I/O發生時的工做狀況

在上面這個讀取文件的例子中

  • 若是在讀取文件的同時,該線程被‘掛起’(能夠理解爲進程的阻塞態),CPU不在關注這個線程直到結果被返回,屬於阻塞式
  • 若是在讀取文件的同時,CPU會時不時關注並檢查一遍結果是否返回,則屬於非阻塞式

若是沒法區分同步阻塞,請參考這裏

異步則解決了代碼被耗時任務阻止其往下執行的缺點

多線程異步有着比較好的解決方案:

  • 給涉及到I/O操做的部分新開一個線程執行
  • 主線程不等待繼續往下執行
  • I/O線程執行完以後將結果寫回公共區並通知主線程(也能夠是主線程去輪詢)
  • 主線程執行其回調

可是

JavaScript是一門單線程語言,自己沒法提供多線程,那麼是經過怎樣的機制來實現異步的?

先給出答案:JavaScript經過事件循環和瀏覽器各線程協調共同實現異步

JavaScript認爲任務分爲兩種,一種是全由CPU決定完成速度的任務,咱們稱其爲同步任務,一種是由多種因素(如硬盤讀取速度,網速,點擊反饋速度)決定完成速度的任務,咱們稱其爲異步任務。

舉個簡單的例子

  • 函數聲明,for循環,變量聲明,賦值操做等均可以屬於同步任務
  • 讀取文件,網絡請求,網頁事件都看作異步任務

JavaScript將全部的異步任務都會放進一個隊列裏面,在執行完全部的同步任務以後,會去隊列中找到最早進入隊列的異步任務執行。

仔細觀察上圖,結合本文在最開始提到的瀏覽器多線程設計:

  • JavaScript線程首先執行同步任務
  • 在執行完同步任務以後,會去異步任務隊列的隊頭取出任務執行
  • 瀏覽器各個線程會在事件觸發且完成事件以後將回調函數寫入異步隊列(先進先出隊列)

由於諸如事件觸發,http請求都是耗時沒法直接肯定的任務,也就是說JavaScript線程沒法得知異步的任務回調函數究竟何時會寫入異步任務隊列,那麼這個地方,就須要一個機制,去時刻輪詢這個任務隊列,這就是事件循環(event loop)

如今咱們再看以下代碼的執行順序:

var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();
複製代碼

真正的執行順序是

var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();    // i am here
    req.onload = function (){};    
    req.onerror = function (){};    
複製代碼

同理

while(1){
console.log('1')
}

setTimeOut(()=>{
	console.log('00000000000000000')
},1)
複製代碼

也將一樣永遠不會輸出00000000000000000

討論下爲何這樣設計

由於JavaScript的工做環境是一個典型的異步應用場景:充斥着各類ajax事件和瀏覽器事件。各個事件的觸發時間和獲得反饋的時間都不得而知,若是設計成同步語言,將會帶來極差的瀏覽器使用體驗。 須要設計一個成一個生產者-消費者模型(也能夠看作是觀察者模式),來管理這樣的異步任務。

瀏覽器須要作的事情太多了,一手須要負責渲染,一手須要負責http請求,一手還須要執行JavaScript,將JavaScript設計成單線程不只可以讓瀏覽器更好地控制各個線程,同時對開發者來講也更簡單。多線程涉及到鎖,臨界區,衝突解決的學習成本仍是比較高的。

總結

再次來看一下這一句話:

JavaScript是異步事件驅動的單線程編程語言

異步:寫代碼順序不必定是執行順序,JavaScript線程先執行同步任務。 事件驅動:其餘線程在各事件完成後將回調函數寫入隊列,都是以抽象事件做爲觸發機制的。 單線程:不能開多線程而是用eventloop來實現異步的。

JavaScript經過事件循環和瀏覽器各線程協調共同實現異步

JavaScript的異步設計很是優秀,這也讓基於V8引擎的Node在服務端大方異彩,可以更加簡單地開發出適合高密集I/O的web應用。

以後會基於JavaScript的EventLoop總結下關於Node的異步I/O(涉及到多線程)

若是喜歡,請關注

最新博客會最早更新在http://www.helloyzy.cn

相關文章
相關標籤/搜索