[前端漫談_4] 從 薛定諤的貓 聊到 Event loop

前言

上次咱們從高階函數聊到了 promise ,此次咱們聊聊:javascript

  • promise A+ 規範和 promise 應用來看 promise 的特性
  • promise 和 eventloop 的關係

從薛定諤的貓(Erwin Schrödinger's Cat)來理解 promise

薛定諤的貓是奧地利著名物理學家薛定諤提出的一個思想實驗,那麼這和 promise 有什麼關係呢?在這個著名的實驗中,假設在盒子裏會有一隻貓,而後咱們打開盒子只會出現兩個結果,貓死了或者是活着:html

那麼 promise 也相似,根據 promise A+ 規範 當一個 promise 被建立出來之後,它就擁有三種可能狀態 Pending (初始時爲 pending)/ Fulfilled / Rejected 若是咱們把範圍放寬一點,那麼 Fulfilled / Rejected 又能夠被稱爲 Settled前端

okay,相信你已經理解了 promise 的三種狀態,那細心同窗看到上面有 then()catch() 這樣的方法可能不理解,咱們再回到上面貓的例子裏面,如今這個科學家比較變態,在第一次實驗以後,貓出現了兩種狀態,可是他並沒結束實驗,而是針對這兩種狀況作了處理並繼續了實驗:html5

與之相似,一個完整的 promise ,在 Pending 狀態發生變化時,只多是兩種狀況,FulfilledRejected,而且咱們能夠看到箭頭是單向的,意味着這個過程是 不可逆 的。java

這意味着,當 Pending 狀態發生了變化,不管是變成 Fulfilled 仍是 Rejected 都沒法再改變了。node

針對這兩種狀況,咱們在 then() 裏面能夠傳入兩個回調函數 onFulfillmentonRejection 做爲來處理不一樣的狀況。git

從圖中咱們能夠看到,當 onFulfillment 時,咱們一般會作一些異步的操做,而 onRejection 一般是作錯誤處理。而後咱們把當前的 promise 從新返回,直到下次他的 then() 再次被執行。github

一個promise.then().then().then() 這樣的方式就是咱們 上一篇文章 中所說的 鏈式調用web

經過 promise 的執行來看特性

經過上一節,咱們已知 promise 自己的幾個特性:vim

  • promise 有三種狀態: Pending (初始時爲 pending)/ Fulfilled / Rejected
  • promise 狀態的轉變是不可逆的: Pending -> Fulfilled 或者 Pending -> Rejected
  • promise 支持 then() 的鏈式調用。

可是還有一些特性,咱們須要從代碼的角度來分析。

1. 建立後,當即執行

由於 promise 原意爲承諾,也就是我預先承諾了未來要達成的一件事情。

因此有同窗會認爲必須等到承諾兌現,也就是 promise 的狀態從 Pending 變爲 Fulfilled 或者 Rejected 時,其構造函數接收的函數纔會被執行。

可是實際上,一個 promise 被建立時,即便咱們沒有定義 then() ,其構造函數接收的函數也會當即執行:

let p = new Promise((resolve, reject) => {
  console.log('A new promise was created1')
  console.log('A new promise was created2')
  console.log('A new promise was created3')
  setTimeout(() => {
    console.log('log setTimeout')
  }, 3000)
  resolve('success')
})

console.log('log outside')
複製代碼

輸出結果:

A new promise was created1
A new promise was created2
A new promise was created3
log outside
log setTimeout
複製代碼

2. 異常處理的方式

根據 promise A+ 規範promisethen() 接收2個參數:

promise.then(onFulfilled, onRejected)
複製代碼

其中 onFulfilled 執行結束後調用,onRejected 拒絕執行後調用,看看這段代碼:

let p = new Promise((resolve, reject) => {
  reject('reject')
  //throw 'error'
})

p.then(
  data => {
    console.log('1:', data)
  },
  reason => {
    console.log('reason:', reason)
  }
)
複製代碼

最後打印的是:

reason: reject
複製代碼

能夠正常運行不是嗎?可是咱們發現實際應用中,咱們並無這樣來定義 then()

p.then(
  data => {
    console.log('1:', data)
  },
  reason => {
    console.log('reason1:', reason)
  }
).then(
  data => {
    console.log('2:', data)
  },
  reason => {
    console.log('reason2:', reason)
  }
).then(
  data => {
    console.log('3:', data)
  },
  reason => {
    console.log('reason3:', reason)
  }
)
複製代碼

而是使用 catch() 配合 onFulfilled()

p.then(data => {
  console.log('1:', data)
}).then(data => {
    console.log('2:', data)
  }).then(data => {
    console.log('3:', data)
  }).catch(e => {
      console.log('e2:', e)
    })
複製代碼

表面上看,達到的效果是同樣的,因此這樣有什麼好處呢?

  1. 減小代碼量。
  2. onFulfilled() 中若是發生錯誤,也會進行捕獲,不會中斷代碼的執行。

3. then() 是異步執行的

看一段代碼:

let p = new Promise((resolve, reject) => {
  console.log('A new promise was created1')
  console.log('A new promise was created2')
  console.log('A new promise was created3')
  resolve('success')
})
console.log('log outside')

p.then(data => {
  console.log('then:', data)
})
複製代碼

執行結果:

A new promise was created1
A new promise was created2
A new promise was created3
log outside
then: success
複製代碼

咱們能夠很清楚的看到,then() 中打印的內容是在最後的,爲何會這樣呢?由於 p.then() 中傳入的函數會被推入到 microtasks(異步任務隊列的一種) 中,而任務隊列都是在執行棧中的代碼(同步任務)以後處理。

下面這些代碼都在同步任務中處理:

console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
console.log('log outside')
複製代碼

okay 看到這裏你可能會有一些問題,例如:

  • 什麼是 同步任務 ?
  • 什麼是 執行棧?
  • 什麼是 microtasks
  • 什麼是 異步任務隊列

要明白這些,就不得不聊聊 Event loop。

Event loop 是什麼?爲何咱們須要 Event loop?

W3C文檔 中咱們能夠找到關於它的描述:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

翻譯一下就是:

客戶端必須使用本章節中所描述的事件循環,來協調事件,用戶交互,腳本,呈現,網絡等等。 事件循環有兩種:用於瀏覽上下文的事件循環和用於 worker 的事件循環。

咱們寫好一段 JavaScript 代碼,而後瀏覽器打開這個頁面,或者在 node 環境中運行它,就能夠獲得咱們指望的結果,可是這段代碼怎麼執行的呢?

不少同窗都知道,是 JavaScript 引擎在執行代碼,而 JavaScript 引擎都是依託於一個宿主環境的,最通用的 JavaScript 宿主環境是瀏覽器。

這和 EventLoop 有什麼關係呢?

由於宿主環境是瀏覽器,因此 JavaScript 引擎被設計爲單線程。

爲何不能是多線程呢?舉個例子:加入咱們同時兩個線程都操做同一個 DOM 元素,那應該如何處理呢?對吧。

okay,既然是單線程,意味着咱們只能順序執行代碼,可是若是咱們執行某一行特別耗費時間,是否是在這行後面的內容就被阻塞了呢?

因此咱們須要在單線程的引擎中來實現異步,而 Event loop 就是實現異步的關鍵。

Event loop 中的任務隊列 & 宏任務 & 微任務

首先當一段代碼給到 JavaScript 引擎的時候,會區分這段代碼是同步仍是異步:

  • 同步的代碼進入主線程執行
  • 異步的代碼加入到任務隊列中,等待主線程通知執行

異步的代碼加入到任務隊列中,而任務隊列又分爲 宏任務隊列(macro tasks)微任務隊列(micro tasks)

一個瀏覽器的上下文環境可能對應有多個宏任務隊列可是隻有一個微任務隊列。你可能以爲會是這樣:

可是實際上,每一個宏任務都包含了一個微任務隊列:

那麼問題來了,咱們怎麼去判斷這段代碼要加入到宏任務隊列,仍是微任務隊列中呢?

咱們參考下文檔 中的解讀:

Each task is defined as coming from a specific task source. All the tasks from one particular task source and destined to a particular event loop

每一個任務都由特殊任務源來定義。 來自同一個特殊任務源的全部任務都將發往特定事件循環

因此咱們能夠按照不一樣的來源進行分類,不一樣來源的任務都對應到不一樣的任務隊列中

  • (macro-task 宏任務)來源:I/O, setTimeout + setInterval + setImmediate, UI renderder ···
  • (micro-task 微任務)來源:Promiseprocess.nextTickMutationObserver, Object.observe ···

明白了這些概念以後,咱們來看看完整的執行過程。

Event loop 完整的執行過程

下圖參考了 Philip Roberts的演講 PPT同時加深和細化:

圖的順序從上往下看:

  1. 代碼開始執行,JavaScript 引擎對全部的代碼進行區分。
  2. 同步代碼被壓入棧中,異步代碼根據不一樣來源加入到宏任務隊列尾部,或者微任務隊列的尾部。
  3. 等待棧中的代碼被執行完畢,此時通知任務隊列,執行位於隊列首部的宏任務。
  4. 宏任務執行完畢,開始執行其關聯的微任務。
  5. 關聯的微任務執行完畢,繼續執行下一個宏任務,直到任務隊列中全部宏任務被執行完畢。
  6. 執行下一個任務隊列。

步驟 3 - 4 - 5 就是一個事件循環的基本原理。

最後

不知道這篇文章有沒有讓你充分理解呢?有任何想法和建議,都留下你的評論吧~

小冊 你不知道的 Chrome 調試技巧 已經開始預售啦。

歡迎關注公衆號 「前端惡霸」,掃碼關注,好貨等着你~

相關文章
相關標籤/搜索