javascript中的Event Loop詳解

首先來一段代碼開篇

    console.log(1);
    
    setTimeout(function() {
      console.log(2);
    });
    
    function fn() {
        console.log(3);
        setTimeout(function() {
          console.log(4);
        }, 2000);
    }
    
    new Promise(function(resolve, reject){
        console.log(5);
        resolve();
        console.log(6);
    }).then(function() {
       console.log(7);
    })
    
    fn();
    console.log(8);

思考一下,能給出準確的輸出順序嗎?

下面一步步的了解,最后看看這塊代碼怎么去執行的。

1.進程,單線程與多線程

進程: 運行的程序就是一個進程,比如你正在運行的瀏覽器,它會有一個進程。

線程: 程序中獨立運行的代碼段。

一個進程由單個或多個線程組成,線程是負責執行代碼的。

學過JS的想必都知道JS是單線程的,那么既然有單線程就有多線程,下面首先看看單線程與多線程的區別。

  • 單線程
    從頭執行到尾,一行一行執行,如果其中一行代碼報錯,那么剩下代碼將不再執行。同時容易代碼阻塞。

  • 多線程
    代碼運行的環境不同,各線程獨立,互不影響,避免阻塞。

2. Event Loop(瀏覽器)

js既然是單線程,那么肯定是排隊執行代碼,那么怎么去排這個隊,就是Event Loop。雖然JS是單線程,但瀏覽器不是單線程。瀏覽器中分為以下幾個線程:

  • js線程
  • UI線程
  • 事件線程(onclick,onchange,...)
  • 定時器線程(setTimeout, setInterval)
  • 異步http線程(ajax)

其中JS線程和UI線程相互互斥,也就是說,當UI線程在渲染的時候,JS線程會掛起,等待UI線程完成,再執行JS線程

  • JS會存在執行棧,從上至下執行js代碼,當遇到異步api時,列如上面所述的各種非JS線程的事件,那么會扔給對應的線程去處理,等處理完畢后,則把回調函數放入事件隊列中,等待執行棧執行完畢,再去讀取事件隊列中的回調函數執行。
    [圖片上傳失敗...(image-fd34d8-1516456564422)]

    • 當一個函數執行,會產生一個新的執行棧,當執行完畢返回上一層執行棧,直到回到全局執行棧
    • 當一個函數調用自己,會產生一個新的執行棧。

整個過程,執行棧,讀取事件隊列就是Event Loop

  • 再來看看promise, 如果對promise不是很了解的同學可以看看另一篇我寫的文章Promise是個什么鬼?實現一個Promise.

    Promise在整個執行中是個特殊的存在,傳入Promise的fn是在當前執行棧中的,會立即執行,但它的then方法是在執行棧之后,事件隊列之前,當然這個和瀏覽器實現有關,大部分瀏覽器是微任務(Microtask),也有瀏覽器放入了宏任務(Macrotask),chorme大哥是放入了微任務,其他紛紛效仿。那大家可能會問什么是微任務?什么是宏任務了?

    • 宏任務(Macrotask) 也就是上面所說的 事件隊列 callback queue
    • 微任務(microtask) 是在執行棧和事件隊列之間 在執行棧之后先清空在微任務中的任務,再去執行事件隊列

3. Node Event Loop

Nodejs是通過V8引擎去解析的,解析后的代碼會去調用node提供的api執行,這些API由libuv這個庫去分配線程執行,最后異步返回給V8引擎。

在Node中提供了2個方法和我們的執行隊列有關

  • process.nextTick

把方法放入執行棧的底部,并不放入宏任務和微任務

   cosnole.log(1);
   
   process.nextTick(function(){
       console.log(2);
   });
   
   new Promise(function() {
       console.log(3);
   }).then(function() {
       console.log(4);
   })
   
   console.log(5);

因為nextTick是放入了執行棧的底部,那么會優先于Promise的then方法,故輸出為1 3 5 2 4

  • setImmediate

把方法放入宏任務的隊列中去,但有一個奇怪的事發生,看下面代碼:

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

大家可以試試把代碼多次執行,發現輸出順序不一定,他們都是放入了宏任務中,但在node文檔中,setImmediate總是排在setTimeout前面,但是在實際中確不一定,不知道是不是一個bug。

4. 講講setTimeout, setInterval

  • 任務隊列與定時器
    上面講到了定時器都是放入了宏任務。如果當前執行棧消耗時間已經大于我們設置的定時器時間,那么定時器的回調在宏任務里,并沒有及時去調用,所有這個時間不是特別準確。
    setTimeout(function(){
        console.log(1);
    }, 2000);
    
    task();

假設task函數執行需要5秒鐘,那么打印1需要在5秒之后再打印,task占用了當前執行棧,要等執行棧執行完畢后再去讀取微任務,等微任務完成,這個時候才會去讀取宏任務里面的setTimeout回調函數執行。setInterval同理,例如每3秒放入宏任務,也要等到執行棧的完成。

  • 定時器自身
    有時候為了延后執行代碼會寫:
    setTimeout(function() {
        console.log(1);
    },0);

但是根據標準這個時候最低是4毫秒,即便現在執行棧已經完成。0是不成立的。寫0瀏覽器為默認為最低毫秒數。

5. 回到開篇的代碼

現在再回到上面的代碼,有答案了嗎?

    // 非異步api,立即執行
    console.log(1);
    
    // 放入全局宏任務
    setTimeout(function() {
      console.log(2);
    });
    
    // 聲明函數,但暫時未調用,不會立馬形成執行棧
    function fn() {
         // 調用fn時立即執行
        console.log(3);
        
        // 放入當前fn執行棧宏任務
        setTimeout(function() {
          console.log(4);
        }, 2000);
    }
    
    new Promise(function(resolve, reject){
        // task任務立即執行
        console.log(5);
        resolve();
        console.log(6);
    }).then(function() {
       // then方法放入微任務
       console.log(7);
    })
    
    // 調用fn進入下個執行棧
    fn();
    
    // fn執行棧完成執行
    console.log(8);

答案就是 1 5 6 3 8 7 2 4

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容