圖解JavaScript——代碼實現【2】(六種異步方案,重點是Promise、Async、發佈/訂閱原理實現,真香)

關注公衆號「 執鳶者」,回覆「 書籍」獲取大量前端學習資料,回覆「 前端視頻」獲取大量前端教學視頻,回覆「 異步」獲取本節總體思惟導圖。

本節主要闡述六種異步方案:回調函數、事件監聽、發佈/訂閱、Promise、Generator和Async。其中重點是發佈/訂閱、Promise、Async的原理實現,經過對這幾點的瞭解,但願咱們前端切圖仔可以在修煉內功的路上更進一步。

1、六種異步方案

1.1 回調函數

異步編程的最基本方法,把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。
  • 優勢:簡單、容易理解和實現。
  • 缺點:屢次調用會使代碼結構混亂,造成回調地獄。
function sleep(time, callback) {
    setTimeout(() => {
        // 一些邏輯代碼
        callback();
    }, time);
}

1.2 事件監聽

異步任務的執行不取決於代碼的執行順序,而取決於某個事件是否發生。
  • 優勢:易於理解,此外對於每一個事件能夠指定多個回調函數,並且能夠「去耦合」,有利於實現模塊化。
  • 缺點:整個程序都要變成事件驅動型,運行流程會變得很不清晰。
dom.addEventListener('click', () => {
    console.log('dom被點擊後觸發!!!');
})

1.3 發佈/訂閱

發佈/訂閱模式在觀察者模式的基礎上,在目標和觀察者之間增長一個調度中心。訂閱者(觀察者)把本身想要訂閱的事件註冊到調度中心,當該事件觸發的時候,發佈者(目標)發佈該事件到調度中心,由調度中心統一調度訂閱者註冊到調度中心的處理代碼。

1.4 Promise

Promise 是異步編程的一種解決方案,是爲解決回調函數地獄這個問題而提出的,它不是新的語法功能,而是一種新的寫法,容許將回調函數的嵌套改成鏈式調用。
  • 優勢:將回調函數的嵌套改成了鏈式調用;使用then方法之後,異步任務的兩端執行看的更加清楚。
  • 缺點:Promise 的最大問題是代碼冗餘,原來的任務被 Promise 包裝了一下,無論什麼操做,一眼看去都是一堆then,原來的語義變得很不清楚。
const promise = new Promise((resolve, reject) => {
    if (/*若是異步成功*/) {
        resolve(value);
    } else {
        reject(error);
    }
});

promise.then((value) => {
    // ...success
}, (reason) => {
    // ...failure
})

1.5 Generator

Generator 函數是ES6提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。其最大特色是能夠控制函數的執行。
  • 優勢:異步操做表示的很簡潔,此外能夠控制函數的執行。
  • 缺點:流程管理不方便,不能實現自動化的流程管理。
function* genF() {
    yield 'come on!';
    yield 'Front End Engineer';
    return 'goood';
}

const gF = genF();
gF.next();// {value: "come on!", done: false}
gF.next();// {value: "Front End Engineer", done: false}
gF.next();// {value: "goood", done: true}
gF.next();// {value: undefined, done: true}

1.6 Async

ES2017 標準引入了async函數,使得異步操做變得更加方便。簡言之,該函數就是Generator函數的語法糖。
  • 優勢:內置執行器,能夠自動執行;語義相比Generator更加清晰;返回值是Promise,比Generator函數的返回值是Iterator對象操做更加方便。
  • 增長學習成本。
async function asyncFun() {
    await func1()
    
    await func2();
    
    return '666';
}
function func1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('888')
        }, 100);
    }).then((value) => {
        console.log(value);
    });
}

function func2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('777')
        });
    }).then((value) => {
        console.log(value);
    });
}

asyncFun().then((value) => {
    console.log(value);
});
// 888
// 777
// 666

2、Promise原理實現

不論是實際開發中仍是面試過程當中,各位老鐵們對Promise確定不會陌生,下面就讓咱們一塊兒來嘮一嘮Promsie的實現原理,根據PromiseA+規範來進行實現,而後對其相關的靜態方法(Promise.resolve()、Promise.reject()、Promise.all()、Promise.race())和實例方法(Promise.prototype.catch()、Promise.prototype.finally())進行實現。

2.1 思考一下

首先用一幅圖來展現一下我考慮實現這個函數的思路吧。

2.2 根據Promise/A+規範實現Promise

人家有相關標準,咱們就要遵照,畢竟遵紀守法纔是好公民,如今只能硬着頭皮把這個標準過一遍。

下面就是基於Promise/A+規範實現的代碼,已經通過promises-aplus-tests庫進行了驗證。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/**
 * Promise構造函數
 * excutor: 內部同步執行的函數
 */
class Promise {
    constructor(excutor) {
        const self = this;
        self.status = PENDING;
        self.onFulfilled = [];// 成功的回調
        self.onRejected = [];// 失敗的回調

        // 異步處理成功調用的函數
        // PromiseA+ 2.1 狀態只能由Pending轉爲fulfilled或rejected;fulfilled狀態必須有一個value值;rejected狀態必須有一個reason值。
        function resolve(value) {
            if (self.status === PENDING) {
                self.status = FULFILLED;
                self.value = value;
                 // PromiseA+ 2.2.6.1 相同promise的then能夠被調用屢次,當promise變爲fulfilled狀態,所有的onFulfilled回調按照原始調用then的順序執行
                self.onFulfilled.forEach(fn => fn());
            }
        }

        function reject(reason) {
            if (self.status === PENDING) {
                self.status = REJECTED;
                self.reason = reason;
                // PromiseA+ 2.2.6.2 相同promise的then能夠被調用屢次,當promise變爲rejected狀態,所有的onRejected回調按照原始調用then的順序執行
                self.onRejected.forEach(fn => fn());
            }
        }

        try {
            excutor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        // PromiseA+ 2.2.1 onFulfilled和onRejected是可選參數
        // PromiseA+ 2.2.5 onFulfilled和onRejected必須被做爲函數調用
        // PromiseA+ 2.2.7.3 若是onFulfilled不是函數且promise1狀態是fulfilled,則promise2有相同的值且也是fulfilled狀態
        // PromiseA+ 2.2.7.4 若是onRejected不是函數且promise1狀態是rejected,則promise2有相同的值且也是rejected狀態
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

        const self = this;
        const promise = new Promise((resolve, reject) => {
            const handle = (callback, data) => {
                // PromiseA+ 2.2.4 onFulfilled或者onRejected須要在本身的執行上下文棧裏被調用,因此此處用setTimeout
                setTimeout(() => {
                    try {
                         // PromiseA+ 2.2.2 若是onFulfilled是函數,則在fulfilled狀態以後調用,第一個參數爲value
                        // PromiseA+ 2.2.3 若是onRejected是函數,則在rejected狀態以後調用,第一個參數爲reason
                        const x = callback(data);
                        // PromiseA+ 2.2.7.1 若是onFulfilled或onRejected返回一個x值,運行這[[Resolve]](promise2, x)
                        resolvePromise(promise, x, resolve, reject);
                    } catch (e) {
                        // PromiseA+ 2.2.7.2 onFulfilled或onRejected拋出一個異常e,promise2必須以e的理由失敗
                        reject(e);
                    }
                })
            }
            if (self.status === PENDING) {
                self.onFulfilled.push(() => {
                    handle(onFulfilled, self.value);
                });

                self.onRejected.push(() => {
                    handle(onRejected, self.reason);
                })
            } else if (self.status === FULFILLED) {
                setTimeout(() => {
                    handle(onFulfilled, self.value);
                })
            } else if (self.status === REJECTED) {
                setTimeout(() => {
                    handle(onRejected, self.reason);
                })
            }
        })

        return promise;
    }
}

function resolvePromise(promise, x, resolve, reject) {
    // PromiseA+ 2.3.1 若是promise和x引用同一對象,會以TypeError錯誤reject promise
    if (promise === x) {
        reject(new TypeError('Chaining Cycle'));
    }

    if (x && typeof x === 'object' || typeof x === 'function') {
        // PromiseA+ 2.3.3.3.3 若是resolvePromise和rejectPromise都被調用,或者對同一個參數進行屢次調用,那麼第一次調用優先,之後的調用都會被忽略。 
        let used;
        try {
            // PromiseA+ 2.3.3.1 let then be x.then
            // PromiseA+ 2.3.2 調用then方法已經包含了該條(該條是x是promise的處理)。
            let then = x.then;

            if (typeof then === 'function') {
                // PromiseA+ 2.3.3.3若是then是一個函數,用x做爲this調用它。第一個參數是resolvePromise,第二個參數是rejectPromise
                // PromiseA+ 2.3.3.3.1 若是resolvePromise用一個值y調用,運行[[Resolve]](promise, y)
                // PromiseA+ 2.3.3.3.2 若是rejectPromise用一個緣由r調用,用r拒絕promise。
                then.call(x, (y) => {
                    if (used) return;
                    used = true;
                    resolvePromise(promise, y, resolve, reject)
                }, (r) => {
                    if (used) return;
                    used = true;
                    reject(r);
                })
            } else {
                // PromiseA+ 若是then不是一個函數,變爲fulfilled狀態並傳值爲x
                if (used) return;
                used = true;
                resolve(x);
            }
        } catch (e) {
            // PromiseA+ 2.3.3.2 若是檢索屬性x.then拋出異常e,則以e爲緣由拒絕promise
            // PromiseA+ 2.3.3.4 若是調用then拋出異常,可是resolvePromise或rejectPromise已經執行,則忽略它
            if (used) return;
            used = true;
            reject(e);
        }

    } else {
        // PromiseA+ 2.3.4 若是x不是一個對象或函數,狀態變爲fulfilled並傳值x
        resolve(x);
    }
}

2.2 其餘方法

按照Promise/A+規範實現了Promise的核心內容,可是其只實現了Promise.prototype.then()方法,那其它方法呢?下面咱們就嘮一嘮其它方法,包括靜態方法(Promise.resolve()、Promise.reject()、Promise.all()、Promise.race())和實例方法(Promise.prototype.catch()、Promise.prototype.finally())。

2.2.1 Promise.resolve()

class Promise {
    // ...
    // 將現有對象轉爲 Promise 對象
    static resolve(value) {
        // 若是參數是 Promise 實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。
        if (value instanceof Promise) return value;

        // 參數是一個thenable對象(具備then方法的對象),Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
        if (typeof value === 'object' || typeof value === 'function') {
            try {
                let then = value.then;
                if (typeof then === 'function') {
                    return new Promise(then.bind(value));
                }
            } catch (e) {
                return new Promise((resolve, reject) => {
                    reject(e);
                })
            }
        }

        // 參數不是具備then方法的對象,或根本就不是對象,Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved。
        return new Promise((resolve, reject) => {
            resolve(value);
        })
    }
}

2.2.2 Promise.reject()

class Promise {
    // ...
    // 返回一個新的 Promise 實例,該實例的狀態爲rejected。
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(reason);
        })
    }
}

2.2.3 Promise.all()

class Promise {
    // ...
    // 用於將多個 Promise 實例,包裝成一個新的 Promise 實例。只有全部狀態都變爲fulfilled,p的狀態纔會是fulfilled
    static all(promises) {
        const values = [];
        let resolvedCount = 0;
        return new Promise((resolve, reject) => {
            promises.forEach((p, index) => {
                Promise.resolve(p).then(value => {
                    resolvedCount++;
                    values[index] = value;
                    if (resolvedCount === promises.length) {
                        resolve(values);
                    }
                }, reason => {
                    reject(reason);
                })
            })
        })
    }
}

2.2.4 Promise.race()

class Promise {
    // ...
     // 只要有一個實例率先改變狀態,狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給回調函數。
    static race(promises) {
        return new Promise((resolve, reject) => {
            promises.forEach((p, index) => {
                Promise.resolve(p).then(value => {
                    resolve(value);
                }, reason => {
                    reject(reason);
                })
            })
        })
    }
}

2.2.5 Promise.catch()

class Promise {
    // ...
    // 是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。
    catch(onRejected) {
        return this.then(undefined, onRejected);
    }
}

2.2.6 Promise.finally()

class Promise {
    // ...
    // 用於指定無論 Promise 對象最後狀態如何,都會執行的操做。
    finally(callback) {
        return this.then(
            value => Promise.resolve(callback()).then(() => value),
            reason => Promise.resolve(callback()).then(() => { throw reason })
        )
    }
}

3、Async原理實現

在開發過程當中經常使用的另外一種異步方案莫過於Async,經過async函數的引入使得異步操做變得更加方便。實質上,async是Generator的語法糖,最大的亮點是async內置執行器,調用後便可自動執行,不像Generator須要調用next()方法才能執行。

這是Async的實現原理,即將Generator函數做爲參數放入run函數中,最終實現自動執行並返回Promise對象。
function run(genF) {
    // 返回值是Promise
    return new Promise((resolve, reject) => {
        const gen = genF();
        function step(nextF) {
            let next;
            try {
                // 執行該函數,獲取一個有着value和done兩個屬性的對象
                next = nextF();
            } catch (e) {
                // 出現異常則將該Promise變爲rejected狀態
                reject(e);
            }

            // 判斷是否到達末尾,Generator函數到達末尾則將該Promise變爲fulfilled狀態
            if (next.done) {
                return resolve(next.value);
            }

            // 沒到達末尾,則利用Promise封裝該value,直到執行完畢,反覆調用step函數,實現自動執行
            Promise.resolve(next.value).then((v) => {
                step(() => gen.next(v))
            }, (e) => {
                step(() => gen.throw(e))
            })
        }

        step(() => gen.next(undefined));
    })
}

4、發佈/訂閱實現

更加詳細內容能夠參考《圖解23種設計模式》前端

發佈/訂閱模式在觀察者模式的基礎上,在目標和觀察者之間增長一個調度中心。訂閱者(觀察者)把本身想要訂閱的事件註冊到調度中心,當該事件觸發的時候,發佈者(目標)發佈該事件到調度中心,由調度中心統一調度訂閱者註冊到調度中心的處理代碼。

// 發佈訂閱(TypeScript版)
interface Publish {
    registerObserver(eventType : string, subscribe : Subscribe) : void;
    remove(eventType : string, subscribe ?: Subscribe) : void;
    notifyObservers(eventType : string) : void;
}
interface SubscribesObject{
    [key : string] : Array<Subscribe>
}
class ConcretePublish implements Publish {
    private subscribes : SubscribesObject;

    constructor() {
        this.subscribes = {};
    }

    registerObserver(eventType : string, subscribe : Subscribe) : void {
        if (!this.subscribes[eventType]) {
            this.subscribes[eventType] = [];
        }

        this.subscribes[eventType].push(subscribe);
    }

    remove(eventType : string, subscribe ?: Subscribe) : void {
        const subscribeArray = this.subscribes[eventType];
        if (subscribeArray) {
            if (!subscribe) {
                delete this.subscribes[eventType];
            } else {
                for (let i = 0; i < subscribeArray.length; i++) {
                    if (subscribe === subscribeArray[i]) {
                        subscribeArray.splice(i, 1);
                    }
                }
            }
        }
    }

    notifyObservers(eventType : string, ...args : any[]) : void {
        const subscribes = this.subscribes[eventType];
        if (subscribes) {
            subscribes.forEach(subscribe => subscribe.update(...args))
        }
    }
}

interface Subscribe {
    update(...value : any[]) : void;
}

class ConcreteSubscribe1 implements Subscribe {
    public update(...value : any[]) : void {
        console.log('已經執行更新操做1,值爲', ...value);
    }
}
class ConcreteSubscribe2 implements Subscribe {
    public update(...value : any[]) : void {
        console.log('已經執行更新操做2,值爲', ...value);
    }
}

function main() {
    const publish = new ConcretePublish();
    const subscribe1 = new ConcreteSubscribe1();
    const subscribe2 = new ConcreteSubscribe2();

    publish.registerObserver('1', subscribe1);
    publish.registerObserver('2', subscribe2);

    publish.notifyObservers('2', '22222');
}

main();
相關章節
圖解JavaScript——代碼實現【1】
圖解JavaScript————基礎篇
圖解JavaScript————進階篇
圖解23種設計模式(TypeScript版)

參考連接
Prmose/A+
Promise源碼實現
ES6入門教程es6

歡迎你們關注公衆號(回覆「異步」獲取本節的思惟導圖,回覆「書籍」獲取大量前端學習資料,回覆「前端視頻」獲取大量前端教學視頻)
面試

相關文章
相關標籤/搜索