具有深度度:JavaScript 事件循環機制 (event loop)

本篇文章已經默認你有了基礎的 ES6 和 javascript語法 知識。

本篇文章比較細緻,如果已經對同步異步,單線程等概念比較熟悉的讀者可以直接閱讀執行棧後面的內容瞭解 event loop 原理

在瞭解 JavaScript 事件循環機制之前,得先了解同步與異步的概念

同步與異步

同步(Sync

constcal =() =>{for(leti =0; i <1e8; i++) {// 做一些運算}}cal();console.log("finish");

同步的含義是如果一個事情沒有做完,則不能執行下一個。

在這裏的例子如果 cal 函數沒有執行完畢 console.log 函數是不會執行的

對於 cal 稱爲 同步函數。

異步 (ASync)

$.ajax("xxx.com",function(res){// ...});console.log("finish");

在上述代碼中,$.ajax 的執行是異步的,不會阻塞 console.log 的運行

即不必等到 $.ajax 請求返回數據後,才執行 console.log

對於 $.ajax 稱爲異步函數。

爲什麼要有異步函數?

單線程

javascript 是一門單線程語言,只能同時做一件事情。

如果沒有異步函數,堵塞在程序的某個地方,會導致後面的函數得不到執行,瀏覽器作爲用戶交互界面,顯然要能及時反映用戶的交互,因此要有異步函數。

爲什麼 javascript 不採用多線程呢?專門派發一個線程去處理用戶交互他不好嗎?

這個你可能得去問 javascript 的作者了。

執行棧

由於 javascript 是單線程語言,因此只有一個執行棧(調用棧)

functionbaz(){console.log("exec")}functionbar(){    baz();}functionfoo(){    bar();}foo();

我們可以用一個動畫來演示執行棧的調用過程

根據動畫流程,我們詳細說一下調用棧的情況

main 函數,也就是把整個 javascript 看成一個函數,入棧

foo 函數被執行,入棧

bar 函數被執行,入棧

baz 函數被執行,入棧

console.log 函數被執行,入棧

console.log 函數執行完畢,出棧

baz 函數執行完畢,出棧

bar 函數執行完畢,出棧

foo 函數執行完畢,出棧

main 函數執行完畢,出棧

這種調用棧可以在程序報錯的時候起到很好的 debug 的作用

functionbaz(){thrownewError("noop!");}functionbar(){    baz();}functionfoo(){    bar();}foo();

在查看錯誤中,我們明顯的看到了之前提到的調用棧。

剛纔的程序並無異步函數,

如果我們在程序中用到了異步函數

console.log("begin");setTimeout(functioncb(){console.log("finish")},1000);

這個時候我們再看執行棧

進棧出棧過程類似上面的分析,可是在這裏,直到 main 函數執行完了,我們都沒看到 cb 函數執行,可是確確實實 1000ms 左右後 cb 函數真的執行了,這裏面是發生了什麼情況?

在解釋這個之前,我們先引入兩個概念

宏觀任務和微觀任務

1. 宏觀任務

在 ES5 之前,異步操作由宿主發起,JavaScript 引擎並不能發起異步操作,這類的異步任務稱爲宏觀任務,比較典型的有

setTimeout(() =>{console.log("exec")},2000);

2.微觀任務

在 ES5 之後出現了 Promise ,用於解決回調地獄的問題,這個函數也是異步的,會等到 fulfill(resolve 或 reject) 後纔會執行 then 方法

newPromise((resolve, reject) =>{    resolve("hello world")}).then(data=>{console.log(data)})

這個異步任務,由 v8 引擎發起 稱爲微觀任務

這兩類任務對 event loop 也有影響

接下來進入本文章重點!!

event loop

event loop 分爲瀏覽器環境和 node 環境,實現是不一樣的,本篇文章暫時只討論瀏覽器環境下的 event loop

1. 瀏覽器環境下的 event loop

接下來,我們具體看一個很大的例子

console.log("1");setTimeout(functioncb1(){console.log("2")},0);newPromise(function(resolve, reject){console.log("3")    resolve();}).then(functioncb2(){console.log("4");})console.log("5")

這段代碼用 event loop 的解釋是這樣的

用文字解釋如下,上述動畫以及文字解釋忽略 main 函數

console.log("1") 入棧出棧,控制檯顯示 1

setTimeout 入棧,加入異步任務隊列(此時處於等待執行完成的狀態,對於setTimeout來說就是等待延遲時間算執行完成,對於Promise 來說就是被 fulfill 了纔算執行完成。

new Promise 入棧出棧,控制檯顯示 3,並且把函數放入異步隊列,等待完成了,就執行 then 方法,這裏的話,演示動畫忘記加上了。

console.log(5) 入棧出棧,控制檯顯示 5

至此,主函數內的任務全部執行完畢,

這裏需要先知道,當任務放入異步任務隊列後他們如果完成了,就會自動進入微觀任務或者宏觀任務隊列。

這個時候 event loop 檢索微觀任務隊列是否有任務,如果有,就拖到 執行棧中執行,如果沒有的話,就檢索宏觀任務隊列是否有任務。

而且,如果一旦微觀任務隊列有任務,就一定會先執行微觀任務隊列的。

如果一旦執行棧有任務就一定會先執行執行棧的。

可以用代碼表述如下

while(true) {while(如果執行棧有任務) {// 執行}if(微觀任務隊列有任務) {// 執行continue;    }if(宏觀任務隊列有任務) {// 執行continue;    }}

至此,我們很容易得到上面的代碼的執行結果是

"1","3","5","4","2"

在做一個宏觀任務嵌套微觀任務的例子加深上述流程的理解。

console.log("1");setTimeout(() =>{console.log("2")newPromise(resolve=>{      resolve()    }).then(() =>{console.log("3")    })},0);setTimeout(() =>{console.log("4")},0);console.log("5")

執行結果會是

"1","5","2","3","4"

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章