我所理解的event loop

靈魂三問

  • JS爲何是單線程的
    • 咱們都知道,JS是單線程的語言,那爲何呢?個人理解是JS設計之初就是爲了在瀏覽器端完成DOM操做和一些簡單交互的,既然涉及到DOM操做若是是多線程就會帶來複雜的同步問題,比較極端的例子就是兩個線程可能一個在刪除某個DOM節點一個卻在修改這個DOM節點,這是瀏覽器以哪一個線程爲準呢?
  • 爲何須要異步
    • 若是沒有異步,單線程的JS從上到下執行遇到一段代碼須要執行比較久時就會阻塞頁面的渲染,形成頁面假死,這顯然是一種不好的用戶體驗
  • 單線程又是如何實現異步的呢
    • 單線程之因此能實現異步是由於在處理異步任務時並非立刻運行的,而是經過一個事件循環(event loop)機制來管理任務的執行。貌似是瀏覽器提供了這麼一個管理任務的event loop線程,它來管理和負責向咱們的主線程上輸送須要執行的任務。因此理解了event loop的機制才能更好地理解JS的運行機制,理解JS的運行機制才能更準確地把握咱們代碼的運行規律。

由一個面試題引起的思考

首先來看一道考察JS執行機制的面試題,原題是今日頭條的前端面試題,我稍微進行了一點改造:html

async function async1() {
  console.log( 'async1 start' )
  await async2()
  console.log( 'async1 end' )
}

async function async2() {
  new Promise( function ( resolve ) {
    console.log( '11' )
    resolve();
  }).then( function () {
    console.log( '22' )
  })
}

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' )

看完以後若是不在瀏覽器端運行一下你能有本身的答案嗎,而且能自圓其說嗎?獲得答案都不難,放在瀏覽器裏天然就有了輸出:前端

script start
async1 start
11
promise1
script end
22
promise2
async1 end
setTimeout

由於題目是變幻無窮的,平常開發中的狀況也是多種多樣的,正確理解了其中的執行規律才能更好地開發,固然若是你獲得的結果是同樣的而且可以自圓其說那也就不必看下去了,若是你還對這個結果有點懵那就聽聽個人理解吧。node

微任務與宏任務

事實上,咱們的JS代碼用同步和異步這兩種劃分方式來決定執行的前後順序顯然是不夠的,從而有了另外一種劃分方式,具體誰提出來的我就沒考證了,大致上你們是這樣分的:面試

  • macro-task(宏任務):總體代碼script,setTimeout,setInterval
  • micro-task(微任務):Promise的then回調,process.nextTick(Node)

由於咱們上面的面試題是前端面試題,因此咱們討論的都是瀏覽器環境下的表現,node環境下的event loop貌似有些不同,這裏就不討論了。segmentfault

event loop的運行機制

既然前端談到了JS是單線程的,同時只能處理一個任務,而咱們又將各類各樣的任務分爲了宏任務和微任務,那到底哪一種任務先執行了,這個運行邏輯就是event loop的判斷邏輯。promise

先說說個人理解,再來印證上面代碼的運行順序:瀏覽器

  • 一段代碼執行時先執行宏任務中的同步代碼
  • 若是遇到像setTimeout這類宏任務就會把代碼方式【宏任務隊列】中
  • 若是遇到像Promise.then()這類微任務會放入【微任務隊列】
  • 在本輪宏任務中的同步代碼執行完以後就會依次執行本輪微任務隊列中的代碼,而後執行下一輪中的宏任務代碼

說回上面的面試題,咱們模擬event loop來首先給他們歸個隊多線程

  • 首先遇到了console.log( 'script start' ),直接打印
  • 而後遇到了setTimeout這個宏任務,就被推到宏任務隊列中
  • 而後是async1(),直接打印同步代碼console.log( 'async1 start' )
  • 關於await async2(),其實是從右到左先執行了async2()裏的代碼,而後遇到await從而交出線程的控制權的,async2()執行以後先打印了11,console.log( '22' )被推入了微任務隊列
  • 接着執行Promise中的同步代碼console.log( 'promise1' ),而後將console.log( 'promise2' )推入微任務隊列
  • 接着打印同步代碼console.log( 'script end' ),到此,全部同步代碼執行完畢
  • 接下來就要執行本輪代碼中的微任務隊列了,因此先打印22,再打印promise2
  • 至此,await等待的async2()執行完了,能夠執行await下面的代碼了,因此接下來打印async1 end
  • 最後,就是執行下一輪event loop中的宏任務setTimeout,最後打印setTimeout

單獨說一下async和await

async本質上是加上了Generator函數而且內置了執行器的一個語法糖,而且async函數返回的是Promise對象。惟一須要注意的是await後面不管接的是同步代碼仍是異步代碼都要等他們執行完畢才能執行await結果以後的代碼。而且通過驗證,當遇到await語句時是從右到左先執行的await後面的代碼,而後才交出線程的控制權直到await等待的結果運行完畢。異步

總結

整體上,算是能對上面那個題目的執行過程有了一個能自圓其說的解釋,只是吧,爲何規則是這樣的呢?這些規則怎麼證僞呢?這是我查資料的時候最糾結的問題。後來跟同事交流了以後吧,以爲也不必糾結,畢竟最終的解釋器是C寫的,不懂規則能夠去看源碼啊,但是我看不懂啊,哈哈。。。因此,既然你們大部分人都是這麼說,也能說得通,暫且先記着吧,至少,仍是可以解釋平常中形形色色代碼的運行規律的。async

看了幾篇不一樣觀點的文章以後從新梳理一下event loop的順序:

  • 先執行宏任務中的同步代碼
  • 執行棧清空以後查詢任務隊列
  • 若是任務隊列中有微任務,先執行
  • 執行完了微任務以後開始下一輪event loop,執行隊列中宏任務的異步代碼

參考文章中有幾篇文章比我講的生動一些,沒看懂的能夠參考一下,我主要是梳理一下本身的理解,有不一樣想法的歡迎交流。

參考文章

相關文章
相關標籤/搜索