javascript中的Event Loop詳解

lihuanji發表於2018-01-23

首先來一段程式碼開篇

    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執行緒的事件,那麼會扔給對應的執行緒去處理,等處理完畢後,則把回撥函式放入事件佇列中,等待執行棧執行完畢,再去讀取事件佇列中的回撥函式執行。

    javascript中的Event Loop詳解

    • 當一個函式執行,會產生一個新的執行棧,當執行完畢返回上一層執行棧,直到回到全域性執行棧
    • 當一個函式呼叫自己,會產生一個新的執行棧。

整個過程,執行棧,讀取事件佇列就是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

相關文章