天天看點

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

讨論event loop要做到以下兩點

  • 首先要确定好上下文,nodejs和浏覽器的event loop是兩個有明确區分的事物,不能混為一談。
  • 其次,讨論一些js異步代碼的執行順序時候,要基于node的源碼而不是自己的臆想。

簡單來講:

  • nodejs的event是基于libuv,而浏覽器的event loop則在html5的規範中明确定義。
  • libuv已經對event loop作出了實作,而html5規範中隻是定義了浏覽器中event loop的模型,具體實作留給了浏覽器廠商。

浏覽器中的event loop

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

浏覽器事件環中js分為兩部分,一個叫heap(堆),一個叫stack(棧)。

對象放在heap(堆)裡,常見的基礎類型和函數放在stack(棧)裡,函數執行的時候在棧裡執行。棧裡函數執行的時候可能會調一些Dom操作,ajax操作和setTimeout定時器,這時候要等stack(棧)裡面的所有程式先走(注意:棧裡的代碼是先進後出),走完後再走WebAPIs,WebAPIs執行後的結果放在callback queue(回調的隊列裡,注意:隊列裡的代碼先放進去的先執行),也就是當棧裡面的程式走完之後,再從任務隊列中讀取事件,将隊列中的事件放到執行棧中依次執行,這個過程是循環不斷的。

  • 1.所有同步任務都在主線程上執行,形成一個執行棧
  • 2.主線程之外,還存在一個任務隊列。隻要異步任務有了運作結果,就在任務隊列之中放置一個事件。
  • 3.一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務隊列,将隊列中的事件放到執行棧中依次執行
  • 4.主線程從任務隊列中讀取事件,這個過程是循環不斷的

整個的這種運作機制又稱為Event Loop(事件循環)

概念中首先要明白是:stack(棧)和queue(隊列)的差別,它們是怎麼去執行的?

棧方法LIFO(Last In First Out):先進後出(先進的後出),典型的就是函數調用。

//執行上下文棧  作用域
var a = "aa";
function one(){
    let a = 1;
    two();
    function two(){
        let b = 2;
        three();
        function three(){
            console.log(b)
        }
    }
}
console.log(a);
one();
           
aa
2           

圖解執行原理:

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

執行棧裡面最先放的是全局作用域(代碼執行有一個全局文本的環境),然後再放one,

one執行再把two放進來,two執行再把three放進來,一層疊一層。

那麼怎麼出呢,怎麼銷毀的呢?

最先走的肯定是three,因為two要是先銷毀了,那three的代碼b就拿不到了,是以是先進後出(先進的後出),是以,three最先出,然後是two出,再是one出。

隊列方法FIFO(First In First Out)

(隊頭)[1,2,3,4](隊尾) 進的時候從隊尾依次進1,2,3,4 出的時候從對頭依次出1,2,3,4

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

浏覽器事件環中代碼執行都是按棧的結果去執行的,但是我們調用完多線程的方法(WebAPIs),這些多線程的方法是放在隊列裡的,也就是先放到隊列裡的方法先執行。

那什麼時候WebAPIs裡的方法會再執行呢?

比如:stack(棧)裡面都走完之後,就會依次讀取任務隊列,将隊列中的事件放到執行棧中依次執行,這個時候棧中又出現了事件,這個事件又去調用了WebAPIs裡的異步方法,那這些異步方法會在再被調用的時候放在隊列裡,然後這個主線程(也就是stack)執行完後又将從任務隊列中依次讀取事件,這個過程是循環不斷的。

下面通過列子來說明:

例子1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3);
})
setTimeout(function(){
    console.log(4);
})
console.log(5);
           
// 結果
1
2
5
3
4           

1、首先執行棧裡面的同步代碼

1

2

5

2、棧裡面的setTimeout事件會依次放到任務隊列中,當棧裡面都執行完之後,再依次從從任務隊列中讀取事件往棧裡面去執行。

3

4

例子2

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
})
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
})
console.log(5)
           
// 結果
1
2
5
3
4
6
7           

3、當執行棧開始依次執行setTimeout時,會将setTimeout裡面的嵌套setTimeout依次放入隊列中,然後當執行棧中的setTimeout執行完畢後,再依次從從任務隊列中讀取事件往棧裡面去執行。

6

7

例子3

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
},400)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},100)
console.log(5)           
// 結果
1
2
5
4
7
3
6           

在例子2的基礎上,如果設定了setTimeout的時間,那就是按setTimeout的成功時間依次執行。

如上:這裡的順序是1,2,5,4,7,3,6。也就是隻要兩個set時間不一樣的時候 ,就set時間短的先走完,包括set裡面的回調函數,再走set時間慢的。(因為隻有當時間到了的時候,才會把set放到隊列裡面去,這一點跟nodejs中的set設定了時間的機制差不多,可以看nodejs中的例子6,也是會先走完時間短,再走時間慢的。)

例子4

當觸發回調函數時,會将回調函數放到隊列中。永遠都是棧裡面執行完後再從任務隊列中讀取事件往棧裡面去執行。

setTimeout(function(){
    console.log('setTimeout')
},4)
for(var i = 0;i<10;i++){
    console.log(i)
}           
// 結果
0
1
2
3
4
5
6
7
8
9
setTimeout           

在學習nodejs事件環之前,我們先了解一下宏任務和微任務在浏覽器中的執行機制。也是面試中經常會被問到的。

宏任務和微任務

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

任務可分為宏任務和微任務,宏任務和微任務都是隊列

  • macro-task(宏任務): setTimeout, setInterval, setImmediate, I/O
  • micro-task(微任務): process.nextTick, 原生Promise(有些實作的promise将then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver不相容的,MessageChannel(消息通道,類似worker)

Promise.then(源碼見到Promise就用setTimeout),then方法不應該放到宏任務中(源碼中寫setTimeout是迫不得已的),預設浏覽器的實作這個then放到了微任務中。例如:

console.log(1)
let promise = new Promise(function(resolve,reject){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(100)
})
console.log(2)
           
1
3
2
100           

先走console.log(1),這裡的new Promise()是立即執行的,是以是同步的,由于這個then在console.log(2)後面執行的,是以不是同步,是異步的。

那這跟宏任務和微任務有什麼關系?

我們可以加一個setTimeout(宏任務)對比一下:

console.log(1)
setTimeout(function(){
    console.log('setTimeout')
},0)
let promise = new Promise(function(resolve,reject){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(100)
})
console.log(2)
           
1
3
2
100
setTimeout           

結論:在浏覽器事件環機制中,同步代碼先執行 執行是在棧中執行的,然後微任務會先執行,再執行宏任務

MutationObserver例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <!-- 當dom加載完畢後,來一句渲染完成 -->
    <script>
        console.log(1)
        let observe = new MutationObserver(function(){
            console.log('渲染完成')
        });
        <!--監控app的節點清單是否渲染完成-->
        observe.observe(app,{
            childList:true
        })
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        console.log(2)
    </script>
</body>
</html>
           
// 結果
1
2
渲染完成           

MessageChannel例子

vue中nextTick的實作原理就是通過這個方法實作的

console.log(1);
let channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.onmessage = function(e){
    console.log(e.data);
}
console.log(2);
port2.postMessage(100);
console.log(3)
           
// 浏覽器中console結果 會等所有同步代碼執行完再執行,是以是微任務晚于同步的
1
2
3
100           

nodejs中的event loop

node的特點:異步 非阻塞i/o node通過LIBUV這個庫自己實作的異步,預設的情況下是沒有異步的方法的。

nodejs中的event loop有6個階段,這裡我們重點關注poll階段(fs的i/o操作,對檔案的操作,i/o裡面的回調函數都放在這個階段)

浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

event loop的每一次循環都需要依次經過上述的階段。 每個階段都有自己的callback隊列,每當進入某個階段,都會從所屬的隊列中取出callback來執行,當隊列為空或者被執行callback的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱為一輪循環。

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
})
           
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise           
浏覽器說:雖然都叫event loop,但是我和node不一樣讨論event loop要做到以下兩點浏覽器中的event loop宏任務和微任務nodejs中的event loop最後問題

1、首先執行完棧裡面的代碼

console.log(1);

console.log(2);

2、從棧進入到event loop的timers階段,由于nodejs的event loop是每個階段的callback執行完畢後才會進入下一個階段,是以會列印出timers階段的兩個setTimeout的回調

setTimeout1

setTimeout2

3、由于node event中微任務不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。是以當times階段的callback執行完畢,準備切換到下一個階段時,執行微任務(列印出Piromise),

Promise

如果例子1看懂了,以下例子2-例子6自己走一遍。需要注意的是例子6,當setTimeout設定了時間,優先按時間順序執行(浏覽器事件環中例子3差不多)。例子7,例子8是重點。

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
           
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise           
console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
           
-> node eventloop.js
1
2
setTimeout2
setTimeout1
Promise           
console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
})
           
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2           

例子5

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
           
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2           

例子6

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
           
-> node eventloop.js
1
2
setTimeout2
Promise2
setTimeout1
Promise1           

例子7:setImmediate() vs setTimeout()

  • setImmediate 設計在poll階段完成時執行,即check階段;
  • setTimeout 設計在poll階段為空閑時,且設定時間到達後執行;但其在timer階段執行

其二者的調用順序取決于目前event loop的上下文,如果他們在異步i/o callback之外調用,其執行先後順序是不确定的

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
           
-> node eventloop.js
timeout
immediate

-> node eventloop.js
immediate
timeout           

但當二者在異步i/o callback内部調用時,總是先執行setImmediate,再執行setTimeout

這是因為fs.readFile callback執行完後,程式設定了timer 和 setImmediate,是以poll階段不會被阻塞進而進入check階段先執行setImmediate,後進入timer階段執行setTimeout

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
           
$ node eventloop.js
immediate
timeout           

例子8:process.nextTick()

process.nextTick()不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。

function Fn(){
    this.arrs;
    process.nextTick(()=>{
        this.arrs();
    })
}
Fn.prototype.then = function(){
    this.arrs = function(){console.log(1)}
}
let fn = new Fn();
fn.then();
           
-> node eventloop.js
   1           

不加process.nextTick,new Fn()的時候,this.arrs是undefind,this.arrs()執行會報錯;

加了process.nextTick,new Fn()的時候,this.arrs()不會執行(因為process.nextTick是微任務,隻有在各個階段切換的中間執行,是以它會等到同步代碼執行完之後才會執行)這個時候同步代碼fn.then()執行=>this.arrs = function(){console.log(1)},this.arrs變成了一個函數,同步執行完後再去執行process.nextTick(()=>{this.arrs();})就不會報錯。

需要注意的是:nextTick千萬不要寫遞歸,可以放一些比setTimeout優先執行的任務

// 死循環,會一直執行微任務,卡機
function nextTick(){
    process.nextTick(function(){
        nextTick();
    })
}
nextTick()
setTimeout(function(){

},499)
           

最後再來段代碼加深了解

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});           
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout           

1、從poll —> check階段,先執行process.nextTick,

nextTick1

nextTick2

2、然後進入check,setImmediate,

setImmediate

3、執行完setImmediate後,出check,進入close callback前,執行process.nextTick

nextTick3

4、最後進入timer執行setTimeout

setTimeout

結論:在nodejs事件環機制中,微任務是在各個階段切換的中間去執行的。

最後

  • 在浏覽器的事件環機制中,我們需要了解的是棧和隊列是怎麼去執行的。

    棧:先進後出;隊列:先進先出。

    所有代碼在棧中執行,棧中的DOM,ajax,setTimeout會依次進入到隊列中,當棧中代碼執行完畢後,有微任務先會将微任務依次從隊列中取出放到執行棧中執行,最後再依次将隊列中的事件放到執行棧中依次執行。

  • 在nodejs的事件環機制中,我們需要了解的是node的執行機制是階段型的,微任務不屬于任何階段,而是在各個階段切換的中間執行。nodejs把事件環分成了6階段,這裡需要注意的是,當執行棧裡的同步代碼執行完畢切換到node的event loop時也屬于階段切換,這時候也會先去清空微任務。
  • 微任務和宏任務

    micro-task(微任務): process.nextTick, 原生Promise(有些實作的promise将then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver不相容的

問題

如果在執行宏任務的過程中又發現了回調中有微任務,會把這個微任務提前到所有宏任務之前,等到這個微任務完成後再繼續執行宏任務嗎?

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
})
setTimeout(function(){
    console.log(3)
    Promise.resolve(1).then(function(){
        console.log('promise2')
    })
})
setTimeout(function(){
    console.log(4)
    Promise.resolve(1).then(function(){
        console.log('promise3')
    })
})
           
// node中 每個階段切換中間執行微任務
1
2
3
4
promise1
promise2
promise3           
// 浏覽器中  先走微任務
1
VM59:3 2
VM59:5 promise1
VM59:9 3
VM59:11 promise2
VM59:15 4
VM59:17 promise3           

以下例子也可以看看

// 例子1
console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')

        setTimeout(function(){
            console.log(3)
            Promise.resolve(1).then(function(){
                console.log('promise2')
            })
        })

    })
})
           
// node
1
2
promise1
3
promise2           
// 浏覽器
1
VM70:3 2
VM70:5 promise1
VM70:8 3
VM70:10 promise2           
// 例子2
console.log(11);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
    setTimeout(function(){
        console.log(3)
        Promise.resolve(1).then(function(){
            console.log('promise2')
        })
    })
})
           
// node
11
2
promise1
3
promise2           
// 浏覽器
11
VM73:4 2
VM73:6 promise1
VM73:9 3
VM73:11 promise2
           

原文釋出時間為:2018年06月04日

原文作者:我是家碧

本文來源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需轉載請聯系原作者

繼續閱讀