JavaScript疑難雜症系列-事件循環

javascript單線程

瀏覽器端,複雜的UI環境會限制多線程語言的開發。
例如,一個線程在操做一個DOM元素時,另外一個線程須要去刪除DOM元素,
這個之間就須要進行狀態的同步,況且前端可能不止操做這麼一個DOM元素。
因此,爲了不在開發過程當中,去進行復雜的同步,選用單線程語言進行開發是最好的解決方案

棧就是和列表相似的一種數據結構,它能夠用來解決計算機世界裏不少的問題。棧是一種高效的數據結構,由於數據只能在棧頂添加或刪除,因此這樣的操做很快,並且容易實現。
棧的使用遍及程序語言的方方面面,從表達式求值處處理函數調用; 函數調用造成了一個棧幀
具備一種後入先出(LIFO,last-in-first-out)的數據結構
因爲棧具備後入先出的特色,全部任何不在棧頂的元素都沒法訪問,爲了拿到棧底的元素,必選先拿掉上面的元素

clipboard.png

可在線演示這段代碼的執行流程javascript

function fun1(){
      return 'hello hip-hop';
    }

    function fun2(){
      return fun1();
    }

    function fun3(){
      console.log(fun2());
    }

    fun3();   //'hello hip-hop'

堆(引用類型)

當咱們在程序中建立一個對象時,這個對象將被保存到運行時數據區中,以便反覆利用(由於對象的建立成本開銷較大),這個運行時數據區就是堆內存。
堆內存中的對象不會隨方法的結束而銷燬,即便方法結束後,這個對象還可能被另外一個引用變量所引用(方法的參數傳遞時很常見),則這個對象依然不會被銷燬,只有當一個對象沒有任何引用變量引用它時,
系統的垃圾回收機制纔會在覈實的時候回收它對象被分配在一個堆中,即用以表示一個大部分非結構化的內存區域

clipboard.png

隊列

排隊在第一個的人先辦理業務,其它人只能排着,直到輪到他們爲止只能在末尾插入元素,在隊首刪除元素。
隊列用於存儲按順序排列的數據 先進先出列被用在不少地方。好比提交操做系統執行一系列進程。打印任務池等。一些仿真系統用來模擬銀行或雜貨店裏排隊的顧客

事件循環機制

clipboard.png

這幅圖片中,咱們能夠看到完整的執行流程,其中涉及到的異步事件有DOM事件、ajax請求和setTimeout。因此,總體的執行流程是這樣子的:html

  1. 全部同步任務會在主線程的調用棧中執行。
  2. 在主線程以外,還有一個任務隊列,一旦指定事件發生以後,異步任務就會被放入任務隊列中
  3. 當主線程執行完調用棧中的同步任務時,會遍歷任務隊列,將任務隊列中的任務放入主線程中執行。而以後事件循環一直會去遍歷任務隊列,一旦有任務放入就會放入主線程中執行。

這樣,咱們就已經初步瞭解了同步和異步之間的實現,以及瀏覽器中的事件循環機制。前端

任務隊列

所謂任務是WebAPIs返回的一個個通知,讓JS主線程在讀取任務隊列的時候得知這個異步任務已經完成,下一步該執行這個任務的回調函數了。

主線程擁有多個任務隊列,不一樣的任務隊列用來排列來自不一樣任務源的任務。
任務源是什麼?像setTimeout/Promise/DOM事件等都是任務源,來自同類任務源的任務咱們稱它們是同源的,好比setTimeout與setInterval就是同源的
在ES6標準中任務隊列又分爲宏觀任務隊列和微觀任務隊列java

clipboard.png

宏觀與微觀任務隊列

ES6標準中任務隊列存在兩種類型,一種就是上邊提到的一些隊列,
宏觀任務隊列(macrotask queue):setTimeout、網絡請求Ajax、用戶IO等
微觀任務隊列(microtask queue),Promise就屬於微觀任務隊列
在執行棧執行的過程當中會把屬於微觀任務隊列的任務分配到相應的微觀任務隊列中去。而在調用棧執行空以後,主線程讀取任務隊列時,會先讀取全部微觀任務隊列,而後讀取一個宏觀任務隊列,再讀取全部的微觀任務隊列

clipboard.png

setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
    console.log(1)
    for( var i=0 ; i<10000 ; i++ ){
        i==9999 && resolve()
    }
    console.log(2)
}).then(function(){
    console.log(5)
});
console.log(3);

  1. 腳本開始執行,最早遇到setTimeout,交給瀏覽器去計時,達到setTimeout限制最短計時以後,把這個任務推入setTimeout隊列。
  2. 遇到Promise構造函數,構造函數參數執行,輸出1,調用resolve改變Promise對象的狀態,而後輸出2。
    Promise對象調用then方法,將這個任務推入Promise任務隊列。
    執行console.log(3),輸出3。
  3. 調用棧爲空,讀取任務隊列,按照 讀取全部微觀任務隊列 -> 執行 ->
  4. 讀取一個宏觀任務隊列 -> 執行 ->
  5. 讀取全部微觀任務隊列 -> 執行 ->
  6. 再讀取一個宏觀任務隊列…的順序。
  7. 讀取全部微觀任務隊列中的任務,執行這些任務指定的回調函數。執行then指定的回調函數,輸出5(微觀任務隊列也具備優先級)。
  8. 最後讀取到setTimeout的任務,執行回調函數,輸出4。

因此最後的輸出順序是1,2,3,5,4,而不是1,2,3,4,5。若是不清楚微觀任務隊列的執行機制,很容易將兩個異步任務歸爲一類,將執行順序判斷錯誤git

零延遲

零延遲並非意味着回調會當即執行。 在零延遲調用 setTimeout 時,其並非過了給定的時間間隔後就立刻執行回調函數。
其等待的時間基於隊列里正在等待的消息數量。 在下面的例子中,"this is just a message" 將會在回調
(callback) 得到處理以前輸出到控制檯, 這是由於延遲是要求運行時 (runtime) 處理請求所需的最小時間,但不是有所保證的時間
(function () {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from call back');
  });

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from call back1');
  }, 0);

  console.log('this is the  end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"

參考
《javascript高級程序設計》
https://developer.mozilla.org...
http://www.cnblogs.com/ahthw/...
https://kongchenglc.github.io...
https://github.com/laizimo/zi...github

相關文章
相關標籤/搜索