從Promise的模擬實現看JS事件循環

本文思路的開始是模擬實現Promise,因此先來探討Promise。vue

Promise 是異步編程的一種解決方案,最先是社區爲了避免在回調地獄裏沉淪而提出,ES6將其寫進了語言標準,統一了用法,原生提供了Promise對象。node

Promise 簡單說就是一個容器,裏面保存着一個異步操做結束後的結果。git

promise.then(data => console.log(data))

// then 表示異步操做完成
// data 就是結果
複製代碼

思考一個問題:如何在異步操做結束後,當即取得其結果?

好比這裏有一個異步操做,用setTimeout模擬:github

let data = null
setTimeout(function() { data = 1 }, 1000) 
複製代碼

當data發生改變後,我想「馬上」輸出。編程

第一種方法(顯而易見):api

setTimeout(function () { 
    let data = 1 
    console.log(data)
}, 1000)
複製代碼

第二種方法(略顯而易見):promise

setTimeout(function() { 
    let data = 1 
    setTimeout(() => console.log(data), 0)
}, 1000)
複製代碼

爲何第二種方式也能取到?瀏覽器

先來讀一遍教科書般(哪都能看到)的《JS事件循環機制》:bash

1.全部任務都在主線程上執行,造成一個執行棧。
2.主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
3.一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列"。那些對應的異步任務,結束等待狀態,進入執行棧並開始執行。
4.主線程不斷重複上面的第三步。
複製代碼

而後咱們來看第二種方法。異步

setTimeout(function() { 
    // 這個function內,對應一個執行棧
    let data = 1 // 同步任務
    setTimeout(() => console.log(data), 0) // 一個異步任務,執行到這時,會將該異步任務先放進"任務隊列"
    // 同步任務 "let data = 1" 執行完,執行異步任務
}, 1000)
複製代碼

這裏調整兩行代碼順序,結果是同樣的。

setTimeout(function() { 
    setTimeout(() => console.log(data), 0) 
    let data = 1
}, 1000)
複製代碼

可是promise.then(data => console.log(data))結構不太同樣,這裏用一個回調函數取得異步操做後的結果。

他是怎麼作到的。

// 簡易 Promise 定義
function Promise(excutor) {
    this.callback = function() {}
    
    let that = this
    function resolve(value) {
        // 放置一個異步任務,在異步任務執行回調
        setTimeout(() => that.callback(value), 0)
    }

    excutor(resolve)
}

// then 方法只是保存callback函數
Promise.prototype.then = function (callback) {
    this.callback = callback
}

const promise = new Promise(function(resolve) {
    setTimeout(() => resolve(1), 1000)
})
promise.then(data => console.log(data))
複製代碼

先把data => console.log(data)函數保存,再在resolve接收到異步數據後執行。

這裏能按照這樣的前後順序,跟上面第二種方法道理是同樣的。

setTimeout(function() { 
    setTimeout(() => console.log(data), 0) // => setTimeout(() => that.callback(value), 0) => 放置異步任務
    let data = 1 // => promise.then(data => console.log(data)) => 都是同步任務
}, 1000)
複製代碼

這大體是promise的基本原理,以上咱們使用setTimeout來實現異步任務,從而達到模擬promise的效果。

談到異步任務,就要引伸出微任務(micro task)和宏任務(macro task)了。

在瀏覽器中,異步任務大體有:

宏任務 (MacroTask):setTimeout、setInterval、I/O、UI渲染
微任務 (MicroTask):Promise、MutationObsever
複製代碼

在node環境中,異步任務大體有:

宏任務 (MacroTask):setTimeout、setInterval、I/O、setImmediate
微任務 (MicroTask):Promise、process.nextTick
複製代碼

在一個執行棧中,會先執行同步代碼,遇到異步任務,會將其壓到"任務隊列"(task queue)中。

分別有"宏任務隊列"、"微任務隊列"。同步代碼執行完,將"微任務隊列"首任務的回調加入執行棧,執行。

循環"微任務隊列",直到隊列空。再循環"宏任務隊列"。

不一樣的"宏任務"、"微任務"之間還有優先級,會影響其執行順序。

回到Promise。

引擎裏實現的Promise,會建立一個"微任務"。而且提供了一些api,Promise會尊崇一些規範。

因此,咱們所說的模擬實現Promise,能夠這樣拆分。

  1. 使用哪一種異步任務來模擬。
  2. Promise完整規範如何實現。

異步任務咱們要看執行環境,有哪些可選。

至於規範,要看 Promise A+規範(原版) or Promise A+規範(翻譯版)

這裏面最難理解的是Promise解決過程[[Resolve]](promise, x)。

Promise 解決過程是一個抽象的操做,其需輸入一個 promise 和一個值,
咱們表示爲 [[Resolve]](promise, x),若是 x 有 then 方法且看上去像一個 Promise ,
解決程序即嘗試使 promise 接受 x 的狀態;不然其用 x 的值來執行 promise 。
複製代碼

能夠看到Promise A+規範是很細節的,要想徹底經過他的測試,必須知足他的全部約束。

這裏有篇文章實現的挺好---Promise原理講解 && 實現一個Promise對象 (遵循Promise/A+規範)

在他的resolve方法裏能夠看到,是用setTimeout來模擬。

其實最好是先用微任務模擬,若是環境不支持,再降級爲宏任務。

這個思路相似與vue中的nextTick源碼傳送門

能夠看到他的降級策略是:

Promise -> MutationObserver -> setImmediate -> setTimeout
複製代碼

nextTick的做用是在數據渲染完成後執行,它的道理是在當前執行棧底放入一個異步任務。

相關參考資料:

  1. 詳解JavaScript中的Event Loop(事件循環)機制
  2. Promise A+規範
  3. vue/next-tick.js
相關文章
相關標籤/搜索