為什么要EventLoop?
JS 作為瀏覽器腳本語言,為了避免復雜的同步問題(例如用戶操作事件以及操作DOM),這就決定了被設計成單線程語言,而且也將會一直保持是單線程的。而在單線程中若是遇到了耗時的操作(IO,定時器,網絡請求)將會一直等待,CPU利用率將會大打折扣,時間大量浪費。所以需要設計一種方案讓一些耗時的操作放在一邊等待,讓后面的函數先執行,於是有了EventLoop的設計。
將任務分為兩種:
- 同步任務
- 異步任務
- 定時器都是異步操作
- 事件綁定都是異步操作
- AJAX中一般采取的異步操作(雖然也可以同步)
- 回調函數(不嚴謹的異步)
阮一峰老師《JavaScript 運行機制詳解:再談Event Loop》
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重復上面的第三步。
任務都會按順序進入調用棧(call stack),即圖1-1的stack,然后按棧的順序依次執行。若全是同步任務,就會正常地順序執行。當遇到異步任務時(其實就是執行到了一個耗時的任務,它發起后,需要它的回調函數等待拿到結果之后才繼續進行)將會放到WebAPIs中(圖1-1),等待這個耗時操作返回結果,也有網友把這個 WebAPIs 稱之為 Event Table。如果異步任務在WebAPIs中等待有了結果(比如setTimeout的時間截止了,xhr得到響應結果了,用戶click事件發生了),就會將這個結果作為一個事件置於任務隊列中。 【或者稱之為:注冊回調函數】
那么任務隊列又是什么?個人認為就是圖中的callback queue,或稱之為 Event Queue 。就是存放了各種耗時操作最后響應結果的各個事件(說白了,就是已經拿到結果的,就會從WebAPIs放到任務隊列里來)
圖 1-1 轉自Philip Roberts的演講《Help, I'm stuck in an event-loop》
搞懂上面兩段話后,就可以談EventLoop的作用了:
- 在調用棧和任務隊列之間進行“輪詢”
- 但輪詢的規則是:只有每當調用棧為空,才能去“詢問”任務隊列中是否有事件需要處理
- 若任務隊列存在事件,則會將該事件相應的回調函數(異步操作)結束等待,置於調用棧中開始執行
- 如果調用棧一直不為空,那就一直不會“詢問”任務隊列
以上過程是不斷循環的,js引擎中,存在一個叫monitoring process的進程,這個進程會不斷的檢查主線程的執行情況,一旦為空,就會去任務隊列檢查有哪些待執行的函數。這里的整個過程可以參考 一個工具 loupe 對整個調用過程進行查看。
圖 1-2 loupe, 也是從其他地方發現的這個東西,很直觀
針對call stack調用棧多說一句:通俗地講,將調用棧比喻為程序員,各個任務比喻為需求,任務隊列比喻為總監。當總監提需求時,程序員就要交接需求過來,然后完成它。如果沒有需求,就一直等待總監給需求。給了就做,不給就等。
搞懂同步任務與異步任務的具體執行流程后,再談談為什么要設計宏任務和微任務。
為什么有宏任務、微任務?
頁面渲染事件,各種IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,我們不能准確地控制這些事件被添加到任務隊列中的位置。但是這個時候突然有高優先級的任務需要盡快執行,那么若只有一種類型的任務就不合適了,所以引入了微任務隊列。
至此,任務隊列已被分為:
- 宏任務隊列,即上文說的任務隊列,callback queue,用於存放宏任務
- 微任務隊列,再開辟一個隊列,用於存放微任務
圖 2-1 微任務Microtask Queue的加入
首先列舉一下哪些是宏任務、哪些是微任務
宏任務:
- script(主代碼)
- setTimeout()
- setInterval()
- postMessage
- I/O
- UI交互事件
- setImmediate(Node.js)
- requestAnimationFrame(瀏覽器)
微任務:
- new Promise().then(回調)
- MutationObserver(html5 新特性)
- process.nextTick(Node.js)在當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")之前----觸發回調函數。也就是說,它指定的任務總是發生在所有異步任務之前
緊接着第一節里說的EventLoop,當時沒有考慮什么宏任務微任務,現在再加入微任務的概念再來考慮整個流程:
- 依舊是在調用棧和任務隊列中輪詢。(此時的任務隊列指的是宏任務隊列)
- 調用棧為空后,優先檢查微任務隊列,如果微任務隊列中存在事件,則加入到調用棧中進行執行(為什么先詢問的是微任務隊列而不是宏任務隊列,在后面解釋)
- 注:如果在執行微任務隊列中的函數時,產生了新的微任務(比如then函數嵌套),則會繼續在本次微任務執行過程中執行下去,直到微任務隊列為空為止(就是說如果期間一直有微任務產生,那就會永遠卡在微任務隊列執行)
- 如果微任務隊列為空,那就取宏任務隊列中的事件加入到調用棧中進行執行
- 若在執行宏任務的時候,產生了新的微任務,就會將該微任務加入到微任務隊列,該微任務隊列將會在下一次宏任務執行之前執行,如圖2-2。
- 循環。
依舊是:兩個任務隊列(宏、微)只有有任務,那么主進程的調用棧就會調過去執行,沒有任務的話,主進程就一直等着,直到又有任務。
圖 2-2 宏任務與微任務的執行順序
注意的是,圖2-2看起來是宏任務先執行,微任務后執行,這僅僅是宏任務與微任務的先后次序,但不代表宏任務優先級比微任務高。事實是微任務的優先級是高於宏任務的。因為圖中的微任務是產生於之前的宏任務的,也就不會一開始就出現幾個微任務。本次宏任務產生的微任務,相較於下一次宏任務,會優先執行這些微任務,搶在下一次宏任務執行之前。自然也就映證了設計微任務的初衷:為了讓某些任務盡快執行。在下面的宏任務微任務的代碼流程分析中,我們可以很顯然地看到每一輪最開始都是先查看微任務隊列的,這些微任務都是產生自上一輪循環的。
總結完整的EventLoop流程:
- 執行一個宏任務(調用棧中沒有就從宏、微任務隊列中獲取)
- 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
- 宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
- 當前微任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
微任務在本次宏任務之后執行,在本次渲染之前執行,在下次宏任務之前執行。(宏任務 -> 微任務 -> 渲染 -> 宏任務)
包含宏任務、微任務的異步代碼分析:
// 知乎作者:Miku // 鏈接:https://zhuanlan.zhihu.com/p/257069622
// 注意:代碼中的process.netxTick 函數存在於Node.js中
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5'); }); }); process.nextTick(function() { console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8'); }); setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12'); }); });
第一輪循環:
1)、首先打印 1 2)、接下來是 setTimeout 是異步任務且是宏任務,加入宏任務隊列,記為 setTimeout1 3)、接下來是 process.nextTick ,是微任務,加入微任務隊列,記為 process1 4)、接下來是 new Promise 里面直接 resolve(7) 所以打印 7,后面的then是微任務,加入微任務隊列,
記為 then1
5)、接下來是 setTimeout,是宏任務,記為 setTimeout2,加入宏任務隊列
第一輪循環打印出的是 1 7
當前宏任務隊列:setTimeout1, setTimeout2
當前微任務隊列:process1, then1第二輪循環:
1)、執行所有微任務 2)、執行process1 打印出 6 3)、執行then1 打印出8 4)、微任務隊列已空,執行完畢,開始執行宏任務隊列 5)、執行 setTimeout1 也就是之前遇到的第一個setTimeout 6)、首先打印出 2 7)、遇到 process.nextTick ,是微任務,加入微任務隊列,記為 process2 8)、new Promise中直接打印4,然后resolve 9)、它的 then,是微任務,加入微任務隊列,記為 then2 第二輪循環結束,當前已經打印出來的是 1 7 6 8 2 4 當前宏任務隊列:setTimeout2 當前微任務隊列:process2, then2
第三輪循環:
1)、執行所有微任務 2)、執行 process2 打印出 3 3)、執行 then2 打印出 5 4)、執行第一個宏任務,即 setTimeout2,即代碼中遇到的第二個setTimeout 5)、首先打印出 9 6)、process 是微任務,加入微任務隊列,記為 process3 7)、new Promise 執行 resolve 打印出 11 8)、then 微任務,加入隊列,記為 then3 第三輪循環結束,當前打印順序為:1 7 6 8 2 4 3 5 9 11 當前宏任務隊列為空 當前微任務隊列:process3,then3
第四輪循環:
1)、執行所有的微任務 2)、執行process3 打印出 10 3)、執行then3 打印出 12
4)、檢查當前宏任務隊列,為空,循環之。 代碼執行結束。 最終打印順序為:1 7 6 8 2 4 3 5 9 11 10 12
參考
- 宏任務與微任務 https://zhuanlan.zhihu.com/p/92460508
- js中的宏任務與微任務 https://zhuanlan.zhihu.com/p/78113300
- 對微任務和宏任務的執行順序的個人理解 https://zhuanlan.zhihu.com/p/257069622
- loupe http://latentflip.com/loupe/
- JavaScript 運行機制詳解:再談Event Loop http://www.ruanyifeng.com/blog/2014/10/event-loop.html
- 譯文:JS事件循環機制(event loop)之宏任務、微任務 https://segmentfault.com/a/1190000014940904
- JavaScript中的Event Loop(事件循環)機制 https://segmentfault.com/a/1190000022805523