javascript中的Event Loop詳解

首先來一段代碼開篇javascript

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);
複製代碼

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

下面一步步的瞭解,最後看看這塊代碼怎麼去執行的。node

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

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

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

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

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

  • 單線程 從頭執行到尾,一行一行執行,若是其中一行代碼報錯,那麼剩下代碼將再也不執行。同時容易代碼阻塞。promise

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

2. Event Loop(瀏覽器)

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

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

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

  • JS會存在執行棧,從上至下執行js代碼,當遇到異步api時,列如上面所述的各類非JS線程的事件,那麼會扔給對應的線程去處理,等處理完畢後,則把回調函數放入事件隊列中,等待執行棧執行完畢,再去讀取事件隊列中的回調函數執行。

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

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

相關文章
相關標籤/搜索