深刻理解JS的事件循環

本文由 dellyoung 獨家受權發佈,若是以爲文章有幫助,歡迎點擊閱讀原文給做者點個贊~前端

前言

「 本文共 8606 字,預計閱讀全文須要 28 分鐘 」」
本文將從萬物初始講起JS世界的運轉規則,也就是事件循環,在這個過程當中你就能明白爲何須要這些規則。有了規則JS世界才能穩穩的運轉起來,因此這些規則很是重要,可是你真的瞭解它們了嗎?面試

閱讀本文前能夠思考下面幾個問題:編程

  • 你理解中的事件循環是怎樣的?
  • 有宏任務了,爲何還要有微任務,它們又有什麼關係?
  • promise很是重要,你能夠手撕promise/A+規範了嗎?
  • async/await底層實現原理是什麼?
    本文將會由淺入深的解答這些問題

深刻理解JS系列

第一節:深刻理解JS的深拷貝
第二節:深刻理解JS的原型和原型鏈
第三節:深刻理解JS的事件循環json

萬物初始

本文基於chromium內核講解」
剛開始讓萬物運轉是件挺容易的事情,畢竟剛開始嘛,也沒什麼復瑣事,好比有以下一系列任務:api

  • 任務1:1 + 2
  • 任務2:3 / 4
  • 任務3:打印出 任務1 和 任務2 結果
    把任務轉換成JS代碼長這樣:
function MainThread() {
    let a = 1 + 2;
    let b = 3 / 4;
    console.log(a + b)
}

JS世界拿到這個任務一看很簡單啊:首先建一條流水線(一個單線程),而後依次處理這三個任務,最後執行完後撤掉流水線(線程退出)就好了。數組

深刻理解JS的事件循環
如今我們的事件循環系統很容易就能處理這幾個任務了,能夠得出:promise

  • 單線程解決了處理任務的問題:若是有一些肯定好的任務,可使用一個單線程來按照順序處理這些任務。
    可是有一些問題:瀏覽器

  • 但並非全部的任務都是在執行以前統一安排好的,不少時候,新的任務是在線程運行過程當中產生的
  • 在線程執行過程當中,想加入一個新任務,可是如今這個線程執行完當前記錄的任務就直接退出了

    世界循環運轉

    要想解決上面的問題,就須要引入循環機制,讓線程持續運轉,再來任務就能執行啦前端框架

轉換成代碼就像這樣微信

function MainThread() {
    while(true){
        ······
    }
}

深刻理解JS的事件循環
如今的JS的事件循環系統就能持續運轉起來啦:

  • 循環機制解決了不能循環執行的問題:引入了循環機制,經過一個 while 循環語句,線程會一直循環執行
    不過又有其餘問題出現了:

  • 別的線程要交給我這個主線程任務,而且還可能短期內交給不少的任務。這時候該如何優化來處理這種狀況呢?

    任務放入隊列

    交給主線程的這些任務,確定得按必定順序執行,而且還要得主線程空閒才能作這些任務,因此就須要先將這些任務按順序存起來,等着主線程有空後一個個執行。

可是如何按順序存儲這些任務呢?

很容易想到用隊列,由於這種狀況符合隊列「先進先出」的特色,也就是說 要添加任務的話,添加到隊列的尾部;要取出任務的話,從隊列頭部去取。

深刻理解JS的事件循環
有了隊列以後,主線程就能夠從消息隊列中讀取一個任務,而後執行該任務,主線程就這樣一直循環往下執行,所以只要消息隊列中有任務,主線程就會去執行。

咱們要注意的是:

  • JavaScript V8引擎是在渲染進程的主線程上工做的
    結果以下圖所示:

深刻理解JS的事件循環
其實渲染進程會有一個IO線程:IO線程負責和其它進程IPC通訊,接收其餘進程傳進來的消息,如圖所示:

深刻理解JS的事件循環
我們如今知道頁面主線程是如何接收外部任務了:

  • 若是其餘進程想要發送任務給頁面主線程,那麼先經過 IPC 把任務發送給渲染進程的 IO 線程,IO 線程再把任務發送給頁面主線程

到如今,其實已經完成chromium內核基本的事件循環系統了:

  • JavaScript V8引擎在渲染進程的主線程上工做
  • 主線程有循環機制,能在線程運行過程當中,能接收並執行新的任務
  • 交給主線程執行的任務會先放入任務隊列中,等待主線程空閒後依次調用
  • 渲染進程會有一個IO線程:IO線程負責和其它進程IPC通訊,接收其餘進程傳進來的消息

    完善運轉規則

    如今已經知道:頁面線程全部執行的任務都來自於任務隊列。任務隊列是「先進先出」的,也就是說放入隊列中的任務,須要等待前面的任務被執行完,纔會被執行。

這就致使兩個問題了:

  • 如何處理高優先級的任務?
  • 如何處理執行時間長的任務?
    如何解決這兩個問題呢?

處理高優先級的任務-微任務
以監聽dom變化爲例,若是dom變化則觸發任務回調,可是若是將這個任務回調放到隊列尾部,等到輪到它出隊列,可能已通過去一段時間了,影響了監聽的實時性。而且若是變化很頻繁的話,往隊列中插入了這麼多的任務,必然也下降了效率。

因此須要一種既能兼顧實時性,又能兼顧效率的方法。

解決方案V8引擎已經給出了:在每一個任務內部,開闢一個屬於該任務的隊列,把須要兼顧實時性和效率的任務,先放到這個任務內部的隊列中等待執行,等到當前任務快執行完準備退出前,執行該任務內部的隊列。我們把放入到這個特殊隊列中的任務稱爲微任務。

這樣既不會影響當前的任務又不會下降多少實時性。

如圖所示以任務1放爲例:

深刻理解JS的事件循環
能夠總結一下:

  • 任務隊列中的任務都是宏觀任務
  • 每一個宏觀任務都有一個本身的微觀任務隊列
  • 微任務在當前宏任務中的JavaScript快執行完成時,也就在V8引擎準備退出全局執行上下文並清空調用棧的時候,V8引擎會檢查全局執行上下文中的微任務隊列,而後按照順序執行隊列中的微任務。
  • V8引擎一直循環執行微任務隊列中的任務,直到隊列爲空纔算執行結束。也就是說在執行微任務過程當中產生的新的微任務並不會推遲到下個宏任務中執行,而是在當前的宏任務中繼續執行。
    咱們來看看微任務怎麼產生?在現代瀏覽器裏面,產生微任務只有兩種方式。

  • 第一種方式是使用 MutationObserver監控某個DOM節點,而後再經過JavaScript來修改這個節點,或者爲這個節點添加、刪除部分子節點,當 DOM 節點發生變化時,就會產生 DOM 變化記錄的微任務。
  • 第二種方式是使用 Promise,當調用 Promise.resolve()或者 Promise.reject() 的時候,也會產生微任務。
    而常見的宏任務又有哪些呢?

  • 定時器類:setTimeout、setInterval、setImmediate
  • I/O操做:好比讀寫文件
  • 消息通道:MessageChannel
    而且咱們要知道:

  • 宿主(如瀏覽器)發起的任務稱爲宏觀任務
  • JavaScript 引擎發起的任務稱爲微觀任務
    處理執行時間長的任務-回調
    要知道排版引擎 Blink和JavaScript引擎 V8都工做在渲染進程的主線程上而且是互斥的。」
    在單線程中,每次只能執行一個任務,而其餘任務就都處於等待狀態。若是其中一個任務執行時間太久,那麼下一個任務就要等待很長時間。

若是頁面上有動畫,當有一個JavaScript任務運行時間較長的時候(好比大於16.7ms),主線程沒法交給排版引擎 Blink來工做,動畫也就沒法渲染來,形成卡頓的效果。這固然是很是糟糕的用戶體驗。想要避免這種問題,就須要用到回調來解決。

從底層看setTimeout實現

到如今已經知道了,JS世界是由事件循環和任務隊列來驅動的。

setTimeout你們都很熟悉,它是一個定時器,用來指定某個函數在多少毫秒後執行。那瀏覽器是怎麼實現setTimeout的呢?

要搞清楚瀏覽器是怎麼實現setTimeout就先要弄明白兩個問題:

  • setTimeout任務存到哪了?
  • setTimeout到時間後怎麼觸發?
  • 取消setTimeout是如何實現的?
    setTimeout任務存到哪了
    首先要清楚,任務隊列不止有一個,Chrome還維護着一個延遲任務隊列,這個隊列維護了須要延遲執行的任務,因此當你經過Javascript調用setTimeout時,渲染進程會將該定時器的回調任務添加到延遲任務隊列中。

回調任務的信息包含:回調函數、當前發起時間、延遲執行時間

具體我畫了個圖:

深刻理解JS的事件循環
setTimeout到時間後怎麼觸發
當主線程執行完任務隊列中的一個任務以後,主線程會對延遲任務隊列中的任務,經過當前發起時間和延遲執行時間計算出已經到期的任務,而後依次的執行這些到期的任務,等到期的任務所有執行完後,主線程就進入到下一次循環中。具體呢我也畫了個圖:

深刻理解JS的事件循環
ps:爲了講清楚,畫配圖真的好累哦,點個贊吧!」
到這就清楚setTimeout是如何實現的了:

  • setTimeout存儲到延遲任務隊列中
  • 當主線程執行完任務隊列中的一個任務後,計算延遲任務隊列中到期到任務,並執行全部到期任務
  • 執行完全部到期任務後,讓出主線程,進行下一次事件循環

    手撕promise

    promise很是重要,新加入的原生api和前端框架都大量使用了promise,promise已然成爲前端的「水」和「電」。

promise解決了什麼問題呢?promise解決的是異步編碼風格的問題。

我們來看,之前咱們的異步代碼長這樣:

let fs = require('fs');

fs.readFile('./dellyoung.json',function(err,data){
  fs.readFile(data,function(err,data){
    fs.readFile(data,function(err,data){
      console.log(data)
    })
  })
})

層層嵌套,環環相扣,想拿到回調結果已經夠費勁了,若是還想進行錯誤處理。。。那簡直太難受了。

而promise出現後,這些問題迎刃而解:

let fs = require('fs');

function getFile(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,function(error,data){
        if(error){
            reject(error)
        }
        resolve(data)
    })
  })
}

getFile('./dellyoung.json').then(data=>{
    return getFile(data) 
}).then(data=>{
    return getFile(data)  
}).then(data=>{
    console.log(data)
}).catch(err=>{
    // 統一錯誤處理
    console.log(err)
})

簡直好用了太多。

能夠發現,使用promise解決了異步回調的嵌套調用和錯誤處理的問題。

你們已經知道promise很是重要了,可是如何徹底學會promise呢?手撕一遍promise天然就貫通啦,我們開始撕,在過程當中抽絲剝繭。

promise/A+規範

咱們如今想寫一個promise,可是誰來告訴怎樣纔算一個合格的promise,不用擔憂,業界是經過一個規則指標來實現promise的,這就是Promise / A+,還有一篇翻譯可供參考【翻譯】Promises / A+規範。

接下來就開始逐步實現吧!

同步的promise

先從一個最簡單的promise實現開始

構造函數

先實現promise的地基:初始化用的構造函數

class ObjPromise {
    constructor(executor) {
        // promise狀態
        this.status = 'pending';
        // resolve回調成功,resolve方法裏的參數值
        this.successVal = null;
        // reject回調成功,reject方法裏的參數值
        this.failVal = null;

        // 定義resolve函數
        const resolve = (successVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'resolve';
            this.successVal = successVal;
        };

        // 定義reject
        const reject = (failVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'reject';
            this.failVal = failVal;
        };

        try {
            // 將resolve函數給使用者
            executor(resolve, reject)
        } catch (e) {
            // 執行拋出異常時
            reject(e)
        }
    }
}

我們先寫一個constructor用來初始化promise。

接下來分析一下:

  • 調用ObjPromise傳入一個函數命名爲executor,executor函數接受兩個參數resolve、reject,能夠理解爲分別表明成功時的調用和失敗時的調用。executor函數通常長這樣(resolve,reject)=>{...}
  • status表明當前promise的狀態,有三種'pending'、'resolve'、'reject'(注:從狀態機考慮的話還有一個額外的初始狀態,表示promise還未執行)
  • successVal和failVal分別表明resolve回調和reject回調攜帶的參數值
  • 函數resolve:初始化的時候經過做爲executor的參數傳遞給使用者,用來讓使用者須要的時候調用,將status狀態從'pending'改爲'resolve'
  • 函數reject:初始化的時候經過做爲executor的參數傳遞給使用者,將status狀態從'pending'改爲'reject'
  • 你可能還發現了函數resolve和函數reject 裏面都有if (this.status !== 'pending') {return;},這是由於resolve或reject只能調用一次,也就是status狀態只能改變一次。

    then方法

    then方法做用:拿到promise中的resolve或者reject的值。

1.基礎版then方法

在class裏面放上以下then方法:

then(onResolved, onRejected) {
    switch (this.status) {
        case "resolve":
            onResolved(this.successVal);
            break;
        case "reject":
            onRejected(this.failVal);
            break;
    }
}

來分析一下:

  • then方法能夠傳入兩個參數,兩個參數都是函數,倆函數就像這樣(val)=>{...}
  • 當status狀態爲'resolve'則調用第一個傳入的函數,傳入的val爲successVal
  • 當status狀態爲'reject'則調用第二個傳入的函數,傳入的val爲failVal
    可是then方法還須要支持鏈式調用的,也就是說能夠這樣:
new Promise((resolve,reject)=>{
    resolve(1);
}).then((resp)=>{
    console.log(resp); // 1
}).then(()=>{
   ...
})

2.使then方法支持鏈式調用

其實支持鏈式核心就是then方法要返回一個新的promise,我們來改造一下實現支持鏈式調用。

then(onResolved, onRejected) {
    // 要返回一個promise對象
    let resPromise;
    switch (this.status) {
        case "resolve":
            resPromise = new ObjPromise((resolve, reject) => {
                try{
                    // 傳入的第一個函數
                    onResolved(this.successVal);
                    resolve();
                }catch (e) {
                    reject(e);
                }
            });
            break;
        case "reject":
            resPromise = new ObjPromise((resolve, reject) => {
                try{
                    // 傳入的第二個函數
                    onRejected(this.failVal);
                    resolve();
                }catch (e) {
                    reject(e);
                }
            });
            break;
    }
    return resPromise;
}

再分析一下:

  • 當status爲'resolve'時,將promise成功resolve的結果successVal,傳遞給第一個方法onResolved(),而後執行onResolved(this.successVal)函數
  • 當status爲'reject'時,過程一直,就很少說啦
  • 重點看它們都會把新建立的promise賦值給then方法,執行完後then方法會返回這個新的promise,這樣就能實現then的鏈式調用了
    3.使then方法的鏈式調用能夠傳參

可是你沒有發現一個問題,我then方法內的第一個參數,也就是onResolved()函數,函數內部的返回值應該是要可以傳遞給下面接着進行鏈式調用的then方法的,以下所示:

new Promise((resolve,reject)=>{
    resolve(1);
}).then((resp)=>{
    console.log(resp); // 1
    return 2; // <<< 關注這行
}).then((resp)=>{
   console.log(resp); // 2 接受到了參數2
})

這該如何實現呢?

其實很簡單:

then(onResolved, onRejected) {

    // 定義這個變量保存要返回的promise對象
    let resPromise;

    switch (this.status) {
        case "resolve":
            resPromise = new ObjPromise((resolve, reject) => {
                try{
                    // 傳入的第一個函數
                    let data = onResolved(this.successVal);
                    resolve(data);
                }catch (e) {
                    reject(e);
                }
            });
            break;
        case "reject":
            resPromise = new ObjPromise((resolve, reject) => {
                try{
                    // 傳入的第二個函數
                    let data = onRejected(this.failVal);
                    resolve(data);
                }catch (e) {
                    reject(e);
                }
            });
            break;
    }
    return resPromise;
}

很簡單:

  • 先保存函數執行的結果,也就是函數的返回值
  • 而後,將返回值傳遞給新的用來返回的promise的resolve(),就能夠將返回值保存到新的promise的successVal
  • 執行出錯的話,固然要將錯誤傳遞給新的用來返回的promise的reject(),將錯誤保存到新的promise的failVal
    4.then傳入參數處理

再看看這段常見的代碼:

new Promise((resolve,reject)=>{
    resolve(1);
}).then((resp)=>{
    console.log(resp); // 1
    return 2; 
}).then((resp)=>{
   console.log(resp); // 2
})

能夠看到,then方法的參數能夠只傳一個,繼續來改造:

then(onResolved, onRejected) {
    const isFunction = (fn) => {
        return Object.prototype.toString.call(fn) === "[object Function]"
    };
    onResolved = isFunction(onResolved) ? onResolved : (e) => e;
    onRejected = isFunction(onRejected) ? onRejected : err => {
        throw err
    };
    ······
}

分析一下:

  • 判斷傳入參數的類型是否是函數
  • 傳入類型是函數的話,那沒毛病,直接用就行
  • 傳入類型不是函數的話,那糟糕啦,我們得分別用(e) => e和(err)=>{throw err}來替換
    到如今promise已經能正常運轉啦,代碼以下:
class ObjPromise {
    constructor(executor) {
        // promise狀態
        this.status = 'pending';
        // resolve回調成功,resolve方法裏的參數值
        this.successVal = null;
        // reject回調成功,reject方法裏的參數值
        this.failVal = null;

        // 定義resolve函數
        const resolve = (successVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'resolve';
            this.successVal = successVal;
        };

        // 定義reject
        const reject = (failVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'reject';
            this.failVal = failVal;
        };

        try {
            // 將resolve函數給使用者
            executor(resolve, reject)
        } catch (e) {
            // 執行拋出異常時
            reject(e)
        }
    }

    then(onResolved, onRejected) {
        const isFunction = (fn) => {
            return Object.prototype.toString.call(fn) === "[object Function]"
        };
        onResolved = isFunction(onResolved) ? onResolved : (e) => e;
        onRejected = isFunction(onRejected) ? onRejected : err => {
            throw err
        };

        // 定義這個變量保存要返回的promise對象
        let resPromise;

        switch (this.status) {
            case "resolve":
                resPromise = new ObjPromise((resolve, reject) => {
                    try{
                        // 傳入的第一個函數
                        let data = onResolved(this.successVal);
                        resolve(data);
                    }catch (e) {
                        reject(e);
                    }
                });
                break;
            case "reject":
                resPromise = new ObjPromise((resolve, reject) => {
                    try{
                        // 傳入的第二個函數
                        let data = onRejected(this.failVal);
                        resolve(data);
                    }catch (e) {
                        reject(e);
                    }
                });
                break;
        }
        return resPromise;
    }
}

你能夠在控制檯運行下面這個測試代碼:

new ObjPromise((resolve,reject)=>{
    resolve(1);
}).then((resp)=>{
    console.log(resp); // 1
    return 2; 
}).then((resp)=>{
   console.log(resp); // 2
})

控制檯會依次打印出 1 2。

5.then返回值處理

到如今同步promise代碼已經沒問題啦,可是還不夠,由於Promise/A+規定:then方法能夠返回任何值,固然包括Promise對象,而若是是Promise對象,咱們就須要將他拆解,直到它不是一個Promise對象,取其中的值。

由於status狀態爲'resolve'和'reject'時都須要進行這樣的處理,因此咱們就能夠把處理過程封裝成一個函數,代碼以下:

then(onResolved, onRejected) {
    ···
    let resPromise;

    switch (this.status) {
        case "resolve":
            resPromise = new ObjPromise((resolve, reject) => {
                try {
                    // 傳入的第一個函數
                    let data = onResolved(this.successVal);
                    this.resolvePromise(data, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            break;
        case "reject":
            resPromise = new ObjPromise((resolve, reject) => {
                try {
                    // 傳入的第二個函數
                    let data = onRejected(this.failVal);
                    this.resolvePromise(data, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            break;
    }
    return resPromise;
}

// data爲返回值
// newResolve爲新的promise的resolve方法
// newReject爲新的promise的reject方法
resolvePromise(data, newResolve, newReject) {
    // 判斷是不是promise,不是直接resolve就行
    if(!(data instanceof ObjPromise)){
        return newResolve(data)
    }
    try {
        let then = data.then;
        const resolveFunction = (newData) => {
            this.resolvePromise(newData, newResolve, newReject);
        };
        const rejectFunction = (err) => {
            newReject(err);
        };
        then.call(data, resolveFunction, rejectFunction)
    } catch (e) {
        // 錯誤處理
        newReject(e);
    }
}

分析一下:

  • 判斷返回值類型,當不是promise時,直接resolve就行
  • 當是promise類型時,用this.resolvePromise(newData, newResolve, newReject)來遞歸的調用then方法,直到data不爲promise,而後resolve結果就行啦
    6.解決then返回值循環引用

如今又有問題了:

若是新的promise出現循環引用的話就永遠也遞歸不到頭了

看看執行下面這個代碼:

let testPromise = new ObjPromise((resolve, reject) => {
    resolve(1);
})
let testPromiseB = testPromise.then((resp) => {
    console.log(resp); // 1
    return testPromiseB;
})

會報錯棧溢出。

解決這個問題的方法就是:經過給resolvePromise()方法傳遞當前新的promise對象,判斷當前新的promise對象和函數執行返回值不一樣就能夠了

class ObjPromise {
    constructor(executor) {
        // promise狀態
        this.status = 'pending';
        // resolve回調成功,resolve方法裏的參數值
        this.successVal = null;
        // reject回調成功,reject方法裏的參數值
        this.failVal = null;

        // 定義resolve函數
        const resolve = (successVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'resolve';
            this.successVal = successVal;
        };

        // 定義reject
        const reject = (failVal) => {
            if (this.status !== 'pending') {
                return;
            }
            this.status = 'reject';
            this.failVal = failVal;
        };

        try {
            // 將resolve函數給使用者
            executor(resolve, reject)
        } catch (e) {
            // 執行拋出異常時
            reject(e)
        }
    }

    resolvePromise(resPromise, data, newResolve, newReject) {
        if (resPromise === data) {
            return newReject(new TypeError('循環引用'))
        }
        if (!(data instanceof ObjPromise)) {
            return newResolve(data)
        }
        try {
            let then = data.then;
            const resolveFunction = (newData) => {
                this.resolvePromise(resPromise, newData, newResolve, newReject);
            };
            const rejectFunction = (err) => {
                newReject(err);
            };
            then.call(data, resolveFunction, rejectFunction)
        } catch (e) {
            // 錯誤處理
            newReject(e);
        }
    }

    then(onResolved, onRejected) {
        const isFunction = (fn) => {
            return Object.prototype.toString.call(fn) === "[object Function]"
        };
        onResolved = isFunction(onResolved) ? onResolved : (e) => e;
        onRejected = isFunction(onRejected) ? onRejected : err => {
            throw err
        };

        // 定義這個變量保存要返回的promise對象
        let resPromise;
        switch (this.status) {
            case "resolve":
                resPromise = new ObjPromise((resolve, reject) => {
                    try {
                        // 傳入的第一個函數
                        let data = onResolved(this.successVal);
                        this.resolvePromise(resPromise, data, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                break;
            case "reject":
                resPromise = new ObjPromise((resolve, reject) => {
                    try {
                        // 傳入的第二個函數
                        let data = onRejected(this.failVal);
                        this.resolvePromise(resPromise, data, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                break;
        }
        return resPromise;
    }
}

能夠在控制檯中調用以下代碼試試啦:

new ObjPromise((resolve, reject) => {
    resolve(1);
}).then((resp) => {
    console.log(resp); // 1
    return 2
}).then((resp) => {
    console.log(resp); // 2
    return new ObjPromise((resolve, reject) => {
        resolve(3)
    })
}).then((resp) => {
    console.log(resp); // 3
});

控制檯會一次打印出 1 2 3

異步的promise

如今我們實現了同步版的promise,可是不少狀況下,promise的resolve或reject是被異步調用的,異步調用的話,執行到then()方法時,當前的status狀態仍是'pending'。這該如何改進代碼呢?

思路其實很簡單:

  • 設置兩個數組,分別存起來then()方法的回調函數onResolved和onRejected
  • 當等到調用了resolve或者reject時,執行對應數組內存入的回調函數便可
  • 另外爲了保證執行順序,等待當前執行棧執行完成,我們還須要給constructor的resolve和reject函數裏面使用setTimeout包裹起來,避免影響當前執行的任務。
    根據這個思路來改造一下promise:
class ObjPromise {
    constructor(executor) {
        // promise狀態
        this.status = 'pending';
        // resolve回調成功,resolve方法裏的參數值
        this.successVal = null;
        // reject回調成功,reject方法裏的參數值
        this.failVal = null;

        // resolve的回調函數
        this.onResolveCallback = [];
        // reject的回調函數
        this.onRejectCallback = [];

        // 定義resolve函數
        const resolve = (successVal) => {
            setTimeout(()=>{
                if (this.status !== 'pending') {
                    return;
                }
                this.status = 'resolve';
                this.successVal = successVal;

                //執行全部resolve的回調函數
                this.onResolveCallback.forEach(fn => fn())
            })
        };

        // 定義reject
        const reject = (failVal) => {
            setTimeout(()=>{
                if (this.status !== 'pending') {
                    return;
                }
                this.status = 'reject';
                this.failVal = failVal;

                //執行全部reject的回調函數
                this.onRejectCallback.forEach(fn => fn())
            })
        };

        try {
            // 將resolve函數給使用者
            executor(resolve, reject)
        } catch (e) {
            // 執行拋出異常時
            reject(e)
        }
    }

    // data爲返回值
    // newResolve爲新的promise的resolve方法
    // newReject爲新的promise的reject方法
    resolvePromise(resPromise, data, newResolve, newReject) {
        if (resPromise === data) {
            return newReject(new TypeError('循環引用'))
        }
        if (!(data instanceof ObjPromise)) {
            return newResolve(data)
        }
        try {
            let then = data.then;
            const resolveFunction = (newData) => {
                this.resolvePromise(resPromise, newData, newResolve, newReject);
            };
            const rejectFunction = (err) => {
                newReject(err);
            };
            then.call(data, resolveFunction, rejectFunction)
        } catch (e) {
            // 錯誤處理
            newReject(e);
        }
    }

    then(onResolved, onRejected) {
        const isFunction = (fn) => {
            return Object.prototype.toString.call(fn) === "[object Function]"
        };
        onResolved = isFunction(onResolved) ? onResolved : (e) => e;
        onRejected = isFunction(onRejected) ? onRejected : err => {
            throw err
        };

        // 定義這個變量保存要返回的promise對象
        let resPromise;
        switch (this.status) {
            case "resolve":
                resPromise = new ObjPromise((resolve, reject) => {
                    try {
                        // 傳入的第一個函數
                        let data = onResolved(this.successVal);
                        this.resolvePromise(resPromise, data, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                break;
            case "reject":
                resPromise = new ObjPromise((resolve, reject) => {
                    try {
                        // 傳入的第二個函數
                        let data = onRejected(this.failVal);
                        this.resolvePromise(resPromise, data, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
                break;
            case "pending":
                resPromise = new ObjPromise((resolve, reject) => {
                    const resolveFunction = () => {
                        try {
                            // 傳入的第一個函數
                            let data = onResolved(this.successVal);
                            this.resolvePromise(resPromise, data, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    };
                    const rejectFunction = () => {
                        try {
                            // 傳入的第二個函數
                            let data = onRejected(this.failVal);
                            this.resolvePromise(resPromise, data, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    };
                    this.onResolveCallback.push(resolveFunction);
                    this.onRejectCallback.push(rejectFunction);
                });
                break;
        }
        return resPromise;
    }
}

能夠用下面代碼測試一下:

new ObjPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 100)
}).then((resp) => {
    console.log(resp); // 1
    return 2
}).then((resp) => {
    console.log(resp); // 2
    return new ObjPromise((resolve, reject) => {
        resolve(3)
    })
}).then((resp) => {
    console.log(resp); // 3
});

咱們如今已經基本完成了Promise的then方法啦。

完善promise

到如今已經完成了promise最核心的兩個方法:constructor方法和then方法。不過Promise/A+還規定了一些其餘的方法,我們繼續來完成。

catch方法

catch()方法就是能夠經過回調函數拿到reject的值,這個好辦,其實then方法已經實現了,轉接一下then方法就好了:

catch(onRejected) {
    return this.then(null, onRejected)
}

這樣就實現了catch()方法

Promise.resolve()/reject()方法

你們確定都見過Promise.resolve()或者Promise.resolve()用法。其實做用就是返回一個新的promise,而且內部調用resolve或者reject。

ObjPromise.resolve = (val) => {
    return new ObjPromise((resolve, reject) => {
        resolve(val)
    })
};

ObjPromise.reject = (val) => {
    return new ObjPromise((resolve, reject) => {
        reject(val)
    })
};

經過這兩種方法,我們能夠將現有的數據很方便的轉換成promise對象

all方法
all方法也是很經常使用的方法,它能夠傳入promise數組,當所有resolve或者有一個reject時,執行結束,固然返回的也是promise對象,來實現一下。

ObjPromise.all = (arrPromise) => {
    return new ObjPromise((resolve, reject) => {
        // 傳入類型必須爲數組
        if(Array.isArray(arrPromise)){
            return reject(new TypeError("傳入類型必須爲數組"))
        }
        // resp 保存每一個promise的執行結果
        let resp = new Array(arrPromise.length);
        // 保存執行完成的promise數量
        let doneNum = 0;
        for (let i = 0; arrPromise.length > i; i++) {
            // 將當前promise
            let nowPromise = arrPromise[i];
            if (!(nowPromise instanceof ObjPromise)) {
                return reject(new TypeError("類型錯誤"))
            }
            // 將當前promise的執行結果存入到then中
            nowPromise.then((item) => {
                resp[i] = item;
                doneNum++;
                if(doneNum === arrPromise.length){
                    resolve(resp);
                }
            }, reject)
        }
    })
};

來分析一下:

  • 傳入promise數組,返回一個新的promsie對象
  • resp用來保存全部promise的執行結果
  • 用instanceof來判斷是不是promise類型
  • 經過調用每一個promise的then方法拿到返回值,而且要傳入reject方法
  • 用doneNum來保存執行完成的promise數量,所有執行完後,經過resolve傳遞執行結果resp,而且將當前promise狀態改成'resolve',後續就能夠經過then方法取值

    race方法

    race方法也偶爾會用到,它能夠傳入promise數組,當哪一個promise執行完,則race就直接執行完,我們來實現一下:

ObjPromise.race = (arrPromise) => {
    return new Promise((resolve, reject) => {
        for (let i = 0; arrPromise.length > i; i++) {
            // 將當前promise
            let nowPromise = arrPromise[i];
            if (!(nowPromise instanceof ObjPromise)) {
                return reject(new TypeError("類型錯誤"))
            };
            nowPromise.then(resolve, reject);
        }
    })
};

來分析一下:

  • 傳入promise數組,返回一個新的promsie對象
  • 用instance來判斷是不是promise類型
  • 調用每一個promise的then方法,並傳遞resolve、reject方法,哪一個先執行完就直接結束了,後續就能夠經過then方法取值
    OK,到如今已經實現了一個本身的promise對象!

從底層看async/await實現

手撕完promise,趁熱再深刻學習一下ES7的新特性async/await。async/await至關牛逼:它是JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的狀況下使用同步代碼實現異步訪問資源的能力,而且使得代碼邏輯更加清晰。接下來我們就來深刻了解下async/await爲何能這麼牛逼。

async/await使用了Generator和Promise兩種技術,Promise我們已經掌握了,因此要再看一看Generator究竟是什麼。

生成器Generator
先了解一下生成器Generator是如何工做的,接着再學習Generator的底層實現機制——協程(Coroutine)

如何工做
生成器函數:生成器函數是一個帶星號函數,並且是能夠暫停執行和恢復執行的

先來看下面這段代碼:

function* genFun() {
    console.log("第一段")
    yield 'generator 1'

    console.log("第二段")
    return 'generator 2'
}

console.log('begin')
let gen = genFun()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')

執行這段代碼,你會發現gen並非一次執行完的,而是全局代碼和gen代碼交替執行。這其實就是生成器函數的特性,它能夠暫停執行,也能夠恢復執行。

再來看下,它是具體是怎麼暫停執行和恢復執行的:

  • 在生成器函數內部執行一段代碼,若是遇到yield關鍵字,那麼JavaScript引擎將返回關鍵字後面的內容給外部,並暫停該函數的執行。
  • 外部函數能夠經過next方法恢復生成器函數的執行。
    可是JavaScript引擎V8是如何實現生成器函數的暫停和恢復呢,接着往下看

生成器原理

想要搞懂生成器函數如何暫停和恢復,要先了解一下協程的概念,協程是一種比線程更加輕量級的存在,能夠把協程當作是跑在線程上的任務:

  • 一個線程上能夠存在多個協程,可是在線程上同時只能執行一個協程。
  • 若是從 A 協程啓動 B 協程,咱們就把 A 協程稱爲 B 協程的父協程。
  • 一個進程能夠擁有多個線程同樣,一個線程也能夠擁有多個協程。
  • 協程不是被操做系統內核所管理,而徹底是由程序所控制(也就是在用戶態執行)。所以協程在性能上要遠高於線程。
    小知識點:線程 核心態,協程 用戶態。也就是說線程被內核調度,協程是由用戶的程序本身調度,系統並不知道有協程的存在」
    下面我畫了個圖來演示上面代碼的執行過程:

深刻理解JS的事件循環
從圖中結合代碼能夠看出協程的規則:

  • 經過調用生成器函數genFun來建立一個協程gen,建立以後,gen協程並無當即執行。
  • 要讓gen協程執行,須要經過調用gen.next()。
  • 當協程正在執行的時候,能夠經過yield關鍵字來暫停gen協程的執行,並返回主要信息給父協程。
  • 若是協程在執行期間,遇到了return,那麼JavaScript引擎會結束當前協程,並將return後面的內容返回給父協程。
    其實規則總的來講:

  • 父協程中執行next(),線程控制權就讓給子協程了
  • 子協程中遇到yield,線程控制權就讓給父協程了
  • 能夠看出父協程和子協程仍是互相謙讓的
    可是用Generator生成器仍是不太好用,咱們但願寫代碼的時候,不要手動控制協程之間的切換,該切換時,JavaScript引擎幫我直接切換好多省事。這時候async/await就登場啦!

再看async/await
已經知道,async/await使用了Generator和Promise兩種技術,其實往低層說就是微任務和協程的應用。如今Generator和Promise都已經深刻理解啦。可是微任務和協程是如何協做實現了async/await呢?

  1. async是什麼:

MDN:async是一個經過異步執行並隱式返回Promise做爲結果的函數。」
能夠執行下面代碼:

async function foo() {
    return 1
}
console.log(foo())  // Promise {<resolved>: 1}

能夠看到調用async聲明的foo()函數返回了一個Promise對象,而且狀態是resolved。

  1. await是什麼:

MDN:await 表達式會暫停當前 async function 的執行,等待 Promise 處理完成。
若 Promise 正常處理(fulfilled),其回調的resolve函數參數做爲 await 表達式的值,繼續執行 async function。
若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常緣由拋出。」
先來看下面這段代碼:

async function foo() {
    console.log(1)
    let a = await 99
    console.log(a)
}
console.log(0)
foo()
console.log(3)

想要知道上面這段代碼執行結果如何,就先看看這段代碼的執行流程圖,我已經畫出來了:

深刻理解JS的事件循環

結合上面這張流程圖,分析一下上面代碼的執行過程:

  • 首先,執行console.log(0)這個語句,打印出來0。
  • 因爲foo函數是被async標記過的,因此當進入該函數的時候,JavaScript 引擎會保存父協程調用棧等信息,而後切換到子協程,執行foo函數中的console.log(1)語句,並打印出 1。
  • 當執行到await 99時,會默認建立一個 Promise 對象,以下:
let newPromise = new Promise((resolve,reject){
  resolve(99)
})

而且在建立的過程當中遇到了resolve(99),JavaScript引擎會將該任務推入微任務隊列。

  • 而後JavaScript引擎暫停當前子協程的執行,將主線程控制權交給父協程。而且還會把這個新建立的Promise返回給父協程
  • 父協程拿到主線程控制權後,首先調用newPromise.then,把回調函數放入到Promise中,這個回調函數是什麼?其實就是至關於生成器函數的next(),調用這個回調函數會調用next(),會將父協程的控制權再交給子協程。
  • 接下來繼續執行父協程的流程,這裏執行console.log(3),並打印出來3。
  • 以後父協程將執行結束,在結束以前,會進入微任務的檢查點,檢查微任務,而後執行微任務隊列,微任務隊列中有resolve(99)的任務等待執行。
  • 執行resolve(99),觸發了以前存入的回調函數,回調函數內有next(),父協程的控制權再交給子協程,並同時將 value值99傳給該子協程。
  • 子協程foo激活以後,會把接收到的value值99賦給了變量a,而後foo協程執行console.log(a),打印出99,執行完成以後,將控制權歸還給父協程。
    上面的就是async/await詳細的執行過程啦,能夠看出JavaScript引擎幫咱們作了好多工做,才能讓咱們將異步代碼寫成同步代碼的格式。

參考

  • 瀏覽器工做原理與實踐
  • Promise之你看得懂的Promise
  • MDN-async
  • MDN-await

    小結

  • 從零開始瞭解了JS世界的事件循環機制
  • 明白了爲何會有微任務,以及宏任務與微任務的關係
  • 掌握瞭如何手撕符合Promise/A+規範的Promise
  • 知道async/await使用了Generator和Promise兩種技術,也就是說它是微任務和協程的應用

相關熱門推薦

這一次,完全弄懂 Promise 原理
面試題:說說事件循環機制(滿分答案來了)
async/await 原理及執行順序分析

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個專業的技術人...
    深刻理解JS的事件循環
相關文章
相關標籤/搜索