Event Loop 是 JavaScript 異步編程的核心思想,也是前端進階必須跨越的一關。同時,它又是面試的必考點,特別是在 Promise 出現以後,各類各樣的面試題層出不窮,花樣百出。這篇文章從現實生活中的例子入手,讓你完全理解 Event Loop 的原理和機制,並能遊刃有餘的解決此類面試題。
先來一道面試題鎮樓html
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');`
你是否有見過此類面試題?接下來讓咱們一步一步搞懂他!前端
首先明確一點,js是一門單線程語言。也就是說同一時間只能作一件事。
JavaScript爲何是單線程?與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?java
因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。
但單線程容易引發阻塞,好比:面試
alert(1); console.log(2); console.log(3); console.log(4);
alert彈框只要不點擊肯定那就永遠不會打印出2,3,4。
爲了防止主線程堵塞,javaScript有了同步和異步的概念。編程
同步:同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
異步:異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。這也就是定時器並不能精確在指定時間後輸出回調函數結果的緣由。promise
具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)瀏覽器
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。數據結構
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。異步
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。async
(4)主線程不斷重複上面的第三步。
當咱們調用一個方法的時候,JavaScript 會生成一個與這個方法對應的執行環境,又叫執行上下文(context)。這個執行環境中保存着該方法的私有做用域、上層做用域(做用域鏈)、方法的參數,以及這個做用域中定義的變量和 this 的指向,而當一系列方法被依次調用的時候。因爲 JavaScript 是單線程的,這些方法就會按順序被排列在一個單獨的地方,這個地方就是所謂執行棧。
"任務隊列"是一個事件的隊列(也能夠理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。
"任務隊列"中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。
所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
咱們注意到,在異步代碼完成後仍有可能要在一旁等待,由於此時程序可能在作其餘的事情,等到程序空閒下來纔有時間去看哪些異步已經完成了。因此 JavaScript 有一套機制去處理同步和異步操做,那就是事件循環 (Event Loop)。
示意圖以下:
以去銀行辦業務爲例,當 5 號窗口櫃員處理完當前客戶後,開始叫號來接待下一位客戶,咱們將每一個客戶比做 宏任務
,接待下一位客戶
的過程也就是讓下一個 宏任務
進入到執行棧。
因此該窗口全部的客戶都被放入了一個 任務隊列
中。任務隊列中的都是 已經完成的異步操做的
,而不是註冊一個異步任務就會被放在這個任務隊列中(它會被放到 Task Table 中)。就像在銀行中排號,若是叫到你的時候你不在,那麼你當前的號牌就做廢了,櫃員會選擇直接跳過進行下一個客戶的業務處理,等你回來之後還須要從新取號。
在執行宏任務時,是能夠穿插一些微任務進去。好比你大爺在辦完業務以後,順便問了下櫃員:「最近 P2P 暴雷很嚴重啊,有沒有其餘穩妥的投資方式」。櫃員暗爽:「又有傻子上鉤了」,而後嘰裏咕嚕說了一堆。
咱們分析一下這個過程,雖然大爺已經辦完正常的業務,但又諮詢了一下理財信息,這時候櫃員確定不能說:「您再上後邊取個號去,從新排隊」。因此只要是櫃員可以處理的,都會在響應下一個宏任務以前來作,咱們能夠把這些任務理解成是 微任務
。
大爺聽罷,揚起 45 度微笑,說:「我就問問。」
櫃員 OS:「艹...」
這個例子就說明了:你大爺永遠是你大爺 在當前微任務沒有執行完成時,是不會執行下一個宏任務的!
總結一下,異步任務分爲 宏任務(macrotask)
與 微任務 (microtask)
。宏任務會進入一個隊列,而微任務會進入到另外一個不一樣的隊列,且微任務要優於宏任務執行。
宏任務:script(總體代碼)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
微任務:Promise.then、 MutaionObserver、process.nextTick (Node.js)
setTimeout(() => { console.log('A'); }, 0); var obj = { func: function() { setTimeout(function() { console.log('B'); }, 0); return new Promise(function(resolve) { console.log('C'); resolve(); }); }, }; obj.func().then(function() { console.log('D'); }); console.log('E');
先把打印結果呈上
再把解釋呈上:
setTimeout
放到宏任務隊列,此時宏任務隊列爲 ['A']setTimeout
放到宏任務隊列,此時宏任務隊列爲 ['A', 'B']'C'
then
放到微任務隊列,此時微任務隊列爲 ['D']console.log('E');
,打印出 'E'
'D'
'A'
和 'B'
再來一個?
let p = new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4); }).then(t => console.log(t)); console.log(3);
打印結果:
Promise.resolve()
的 then() 方法放到微任務隊列,此時微任務隊列爲 ['2']4
p
的 then() 方法放到微任務隊列,此時微任務隊列爲 ['2', '1']3
2
和 1
async/await 僅僅是生成器的語法糖,因此不要怕,只要把它轉換成 Promise 的形式便可。下面這段代碼是 async/await 函數的經典形式。
async function foo() { // await 前面的代碼 await bar(); // await 後面的代碼 } async function bar() { // do something... } foo();
其中 await 前面的代碼
是同步的,調用此函數時會直接執行;而 await bar();
這句能夠被轉換成 Promise.resolve(bar())
;await 後面的代碼
則會被放到 Promise 的 then() 方法裏。所以上面的代碼能夠被轉換成以下形式,這樣是否是就很清晰了?
function foo() { // await 前面的代碼 Promise.resolve(bar()).then(() => { // await 後面的代碼 }); } function bar() { // do something... } foo();
最後咱們回到開篇那個題目
function async1() { console.log('async1 start'); // 2 Promise.resolve(async2()).then(() => { console.log('async1 end'); // 6 }); } function async2() { console.log('async2'); // 3 } console.log('script start'); // 1 setTimeout(function() { console.log('settimeout'); // 8 }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); // 4 resolve(); }).then(function() { console.log('promise2'); // 7 }); console.log('script end'); // 5
script start
settimeout
添加到宏任務隊列,此時宏任務隊列爲 ['settimeout']
async1
,先打印出 async1 start
,又由於 Promise.resolve(async2())
是同步任務,因此打印出 async2
,接着將 async1 end
添加到微任務隊列,,此時微任務隊列爲 ['async1 end']promise1
,將 promise2
添加到微任務隊列,,此時微任務隊列爲 ['async1 end', promise2]
script end
async1 end
和 promise2
settimeout
Node.js 在升級到 11.x 後,Event Loop 運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval 和 setImmediate) 就馬上執行微任務隊列,這點就跟瀏覽器端一致。
const p1 = new Promise((resolve, reject) => { console.log('promise1'); resolve(); }) .then(() => { console.log('then11'); new Promise((resolve, reject) => { console.log('promise2'); resolve(); }) .then(() => { console.log('then21'); }) .then(() => { console.log('then23'); }); }) .then(() => { console.log('then12'); }); const p2 = new Promise((resolve, reject) => { console.log('promise3'); resolve(); }).then(() => { console.log('then31'); });
promise1
then11
,promise2
添加到微任務隊列,此時微任務隊列爲 ['then11', 'promise2']
promise3
,將 then31
添加到微任務隊列,此時微任務隊列爲 ['then11', 'promise2', 'then31']
then11
,promise2
,then31
,此時微任務隊列爲空then21
和 then12
添加到微任務隊列,此時微任務隊列爲 ['then21', 'then12']
(由於then21和then12都是第二層then)then21
,then12
,此時微任務隊列爲空then23
添加到微任務隊列,此時微任務隊列爲 ['then23']
then23
這道題實際在考察 Promise 的用法,當在 then() 方法中返回一個 Promise,p1 的第二個完成處理函數就會掛在返回的這個 Promise 的 then() 方法下,所以輸出順序以下。
const p1 = new Promise((resolve, reject) => { console.log('promise1'); // 1 resolve(); }) .then(() => { console.log('then11'); // 2 return new Promise((resolve, reject) => { console.log('promise2'); // 3 resolve(); }) .then(() => { console.log('then21'); // 4 }) .then(() => { console.log('then23'); // 5 }); }) .then(() => { console.log('then12'); //6 });
將不斷更新完善,歡迎批評指正!
http://www.ruanyifeng.com/blo...
https://juejin.im/post/5cbc0a...