異步編程解決方案:Promise

前言

Promise是異步編程的經常使用方案,解決了ajax請求數據中的地獄回調問題,使代碼看起來像同步同樣,可讀性高。本文從梳理知識點出發,參(chao)考(xi)了不少比較比如較權威的技術文對Promise的介紹以及Promise的實現方案,最後輔以大廠的常見異步面試題做爲知識補充。javascript

本文目錄

  • Promise 介紹與基本用法
  • Promise 靜態方法(Static Method)
  • Promise 原型方法(API)
  • Promise 優缺點
  • Promise 實現
  • Async await
  • 異步相關面試題

正文

Promise介紹與基本用法

Promise是什麼

Promise是異步編程的一種解決方案,它表明了一個異步操做的最終完成或者失敗。從語法層面上來說,Promise是一個對象,從它能夠獲取異步操做的消息。前端

Promise對象有如下兩個特色:java

  • 1.對象狀態不受外界影響。node

    Promise對象有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,以決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。jquery

  • 2.狀態改變後不會再變,會一直保持這個結果。ios

    Promsie對象狀態只能由 pending => fulfilled/rejected, 一旦修改就不能再變。若是狀態改變已經發生了,再對Promise對象添加回調函數,也會當即獲得這個結果。git

Promise基本用法

Promise是一個構造函數,new Promise 返回一個 promise 的實例對象,接收一個excutor執行函數做爲參數, excutor有兩個函數類型形參resolve、reject。es6

new Promise((resolve, reject) => {
    // 根據處理結果調用resolve()或reject()
})
複製代碼

來看一個用Promise實現Ajax操做的栗子github

const getJSON = function(url) {
    const promise = new Promise((resolve, reject) => {
        const handler = function() {
            if (this.readyState !== 4) {
                return;
            }
            if (this.status === 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        };
        
        const client = new XMLHttpRequest();
        client.open('GET', url);
        client.onreadystatechange = handler;
        client.responseType = 'json';
        client.setRequestHeader('Accept', 'application/json');
        client.send();
    });
    return promise;
}

getJSON('/posts.json').then((json) => {
    console.log(`Contents:${json}`);
}, (error) => {
    console.error('出錯了', error);
});
複製代碼

上面代碼中,getJSON是對XMLHttpRequest對象的封裝,用於發出一個針對JSON數據的http請求,而且返回一個Promise對象。請求的數據狀態碼爲200的時候,會調用resolve函數,不然調用reject函數,而且調用時都帶有參數,將結果傳遞給回調函數。面試


Promise靜態方法

咱們能夠把Promise構造函數打印出來看看:

(1)Promise.all()

Promise.all方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例,一般用於處理多個並行異步操做。

const p = Promise.all([p1, p2, p3]);
複製代碼

Promise.all方法接受一個數組做爲參數,p一、p二、p3都是 Promise 實例。(Promise.all方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。)

p的狀態由p一、p二、p3決定,分紅兩種狀況。

(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

const p = Promise.all([p1, p2, p3]).then(data => {
    console.log(data); // [1, 2, 3] 返回的結果順序和Promise實例數組順序同樣
}).catch(err => throw err);
複製代碼
(2)Promise.race()

Promise.race()一樣是將多個Promise實例包裝成一個新的Promise實例,與Promise.all()不一樣的是,Promise實例數組中只要有一個實例率先改變狀態,新的Promise實例的回調函數就會返回那個率先改變的Promise實例的返回值。

const p = Promise.race([
    fetch('/api/user-info'),
    new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('request timeout'), 5000)
    }
]);

p
    .then(res => console.log(res)) // 若是5秒內fetch方法返回結果,p的狀態就會變成fulfilled,觸發then方法的回調
    .catch(err => throw err); // 若是5秒後fetch方法沒有返回結果,p的狀態就會變成rejected,從而觸發catch方法的回調
複製代碼
(3)Promise.resolve()

返回一個fulfilled狀態的promise對象

Promise.resolve('hello');
// 至關於
const promise = new Promise(resolve => {
   resolve('hello');
});
複製代碼

參數有一下四種狀況:

  • 1.Promise實例 若是參數是 Promise 實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。
  • 2.thenable對象(具備then方法的對象) Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
let thenable = {
  then: function(resolve, reject) {
    resolve('jiaxin');
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // jiaxin
});
複製代碼
  • 3.原始值(string, number, date等),或者是不具有then方法的對象,返回一個Promise對象
const p = Promise.resolve('Hello');

p.then(function (s){
 console.log(s); // Hello
});
複製代碼
  • 4.不帶任何參數,直接返回一個resolved狀態的 Promise 對象。

    當即resolve()的 Promise 對象,是在本輪「事件循環」(event loop)的結束時執行,而不是在下一輪「事件循環」的開始時。

setTimeout(function () { // 在下一輪「事件循環」開始時執行
  console.log('three');
}, 0);

Promise.resolve().then(function () { // 在本輪事件循環結束時執行
  console.log('two');
});

console.log('one');

// one
// two
// three
複製代碼
Promise.resolve();
複製代碼
(4)Promise.reject()
Promise.reject(reason)方法返回一個rejected狀態的Promise實例
複製代碼
const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s); // 出錯了
});
複製代碼

Promise原型上的方法

咱們把Promise的原型打印出來看看

(1)Promise.prototype.then(onFulfilled, onRejected)
onFulfilled 是用來接收promise成功的值
onRejected 是用來接收promise失敗的緣由
該方法返回一個新的Promise對象,所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。
複製代碼
(2)Promise.prototype.catch(onRejected)
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。返回的也是一個新的promise對象
複製代碼
// bad,不推薦
promise
    .then(data => /** success */, err => /** error */);
    
// good,推薦
promise.then(data => /** success */).catch(err => /** error */);
複製代碼

上述代碼中,第二種寫法要好於第一種寫法,理由是第二種寫法能夠捕獲前面then方法執行中的錯誤,也更接近同步的寫法(try/catch)。所以,建議老是使用catch方法,而不使用then方法的第二個參數。

(3)Promise.prototype.finally(onFinally)

finally方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。

promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
複製代碼

finally方法的回調函數不接受任何參數,所以不知道前面的 Promise 狀態究竟是fulfilled仍是rejected。這代表,finally方法裏面的操做,應該是與狀態無關的,不依賴於 Promise 的執行結果。

而且finally方法老是會返回原來的值


Promise優缺點

  • 優勢:將異步操做以同步操做的流程表達出來,更好地解決了層層嵌套的回調地獄

  • 缺點:

    1.沒法取消Promise,Promise一旦新建即當即執行,沒法中途取消。
    2.若是不設置回調函數,Promise內部拋出的錯誤,不會反映到外部。
    3.當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。


實現Promise

/**
 * Promise 實現 遵循promise/A+規範
 * Promise/A+規範譯文:
 * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
 */

// promise 三個狀態
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let that = this; // 緩存當前promise實例對象
    that.status = PENDING; // 初始狀態
    that.value = undefined; // fulfilled狀態時 返回的信息
    that.reason = undefined; // rejected狀態時 拒絕的緣由
    that.onFulfilledCallbacks = []; // 存儲fulfilled狀態對應的onFulfilled函數
    that.onRejectedCallbacks = []; // 存儲rejected狀態對應的onRejected函數

    function resolve(value) { // value成功態時接收的終值
        if(value instanceof Promise) {
            return value.then(resolve, reject);
        }

        // 爲何resolve 加setTimeout?
        // 2.2.4規範 onFulfilled 和 onRejected 只容許在 execution context 棧僅包含平臺代碼時運行.
        // 注1 這裏的平臺代碼指的是引擎、環境以及 promise 的實施代碼。實踐中要確保 onFulfilled 和 onRejected 方法異步執行,且應該在 then 方法被調用的那一輪事件循環以後的新執行棧中執行。

        setTimeout(() => {
            // 調用resolve 回調對應onFulfilled函數
            if (that.status === PENDING) {
                // 只能由pending狀態 => fulfilled狀態 (避免調用屢次resolve reject)
                that.status = FULFILLED;
                that.value = value;
                that.onFulfilledCallbacks.forEach(cb => cb(that.value));
            }
        });
    }

    function reject(reason) { // reason失敗態時接收的拒因
        setTimeout(() => {
            // 調用reject 回調對應onRejected函數
            if (that.status === PENDING) {
                // 只能由pending狀態 => rejected狀態 (避免調用屢次resolve reject)
                that.status = REJECTED;
                that.reason = reason;
                that.onRejectedCallbacks.forEach(cb => cb(that.reason));
            }
        });
    }

    // 捕獲在excutor執行器中拋出的異常
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

/**
 * resolve中的值幾種狀況:
 * 1.普通值
 * 2.promise對象
 * 3.thenable對象/函數
 */

/**
 * 對resolve 進行改造加強 針對resolve中不一樣值狀況 進行處理
 * @param  {promise} promise2 promise1.then方法返回的新的promise對象
 * @param  {[type]} x         promise1中onFulfilled的返回值
 * @param  {[type]} resolve   promise2的resolve方法
 * @param  {[type]} reject    promise2的reject方法
 */
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {  // 若是從onFulfilled中返回的x 就是promise2 就會致使循環引用報錯
        return reject(new TypeError('循環引用'));
    }

    let called = false; // 避免屢次調用
    // 若是x是一個promise對象 (該判斷和下面 判斷是否是thenable對象重複 因此無關緊要)
    if (x instanceof Promise) { // 得到它的終值 繼續resolve
        if (x.status === PENDING) { // 若是爲等待態需等待直至 x 被執行或拒絕 並解析y值
            x.then(y => {
                resolvePromise(promise2, y, resolve, reject);
            }, reason => {
                reject(reason);
            });
        } else { // 若是 x 已經處於執行態/拒絕態(值已經被解析爲普通值),用相同的值執行傳遞下去 promise
            x.then(resolve, reject);
        }
        // 若是 x 爲對象或者函數
    } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
        try { // 是不是thenable對象(具備then方法的對象/函數)
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, reason => {
                    if(called) return;
                    called = true;
                    reject(reason);
                })
            } else { // 說明是一個普通對象/函數
                resolve(x);
            }
        } catch(e) {
            if(called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

/**
 * [註冊fulfilled狀態/rejected狀態對應的回調函數]
 * @param  {function} onFulfilled fulfilled狀態時 執行的函數
 * @param  {function} onRejected  rejected狀態時 執行的函數
 * @return {function} newPromsie  返回一個新的promise對象
 */
Promise.prototype.then = function(onFulfilled, onRejected) {
    const that = this;
    let newPromise;
    // 處理參數默認值 保證參數後續可以繼續執行
    onFulfilled =
        typeof onFulfilled === "function" ? onFulfilled : value => value;
    onRejected =
        typeof onRejected === "function" ? onRejected : reason => {
            throw reason;
        };

    // then裏面的FULFILLED/REJECTED狀態時 爲何要加setTimeout ?
    // 緣由:
    // 其一 2.2.4規範 要確保 onFulfilled 和 onRejected 方法異步執行(且應該在 then 方法被調用的那一輪事件循環以後的新執行棧中執行) 因此要在resolve里加上setTimeout
    // 其二 2.2.6規範 對於一個promise,它的then方法能夠調用屢次.(當在其餘程序中屢次調用同一個promise的then時 因爲以前狀態已經爲FULFILLED/REJECTED狀態,則會走的下面邏輯),因此要確保爲FULFILLED/REJECTED狀態後 也要異步執行onFulfilled/onRejected

    // 其二 2.2.6規範 也是resolve函數里加setTimeout的緣由
    // 總之都是 讓then方法異步執行 也就是確保onFulfilled/onRejected異步執行

    // 以下面這種情景 屢次調用p1.then
    // p1.then((value) => { // 此時p1.status 由pending狀態 => fulfilled狀態
    //     console.log(value); // resolve
    //     // console.log(p1.status); // fulfilled
    //     p1.then(value => { // 再次p1.then 這時已經爲fulfilled狀態 走的是fulfilled狀態判斷裏的邏輯 因此咱們也要確保判斷裏面onFuilled異步執行
    //         console.log(value); // 'resolve'
    //     });
    //     console.log('當前執行棧中同步代碼');
    // })
    // console.log('全局執行棧中同步代碼');
    //

    if (that.status === FULFILLED) { // 成功態
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try{
                    let x = onFulfilled(that.value);
                    resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一個onFulfilled的返回值
                } catch(e) {
                    reject(e); // 捕獲前面onFulfilled中拋出的異常 then(onFulfilled, onRejected);
                }
            });
        })
    }

    if (that.status === REJECTED) { // 失敗態
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

    if (that.status === PENDING) { // 等待態
        // 當異步調用resolve/rejected時 將onFulfilled/onRejected收集暫存到集合中
        return newPromise = new Promise((resolve, reject) => {
            that.onFulfilledCallbacks.push((value) => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
            that.onRejectedCallbacks.push((reason) => {
                try {
                    let x = onRejected(reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
};

/**
 * Promise.all Promise進行並行處理
 * 參數: promise對象組成的數組做爲參數
 * 返回值: 返回一個Promise實例
 * 當這個數組裏的全部promise對象所有變爲resolve狀態的時候,纔會resolve。
 */
Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
            }, reject)
        })
    })
}

function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function(i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}

/**
 * Promise.race
 * 參數: 接收 promise對象組成的數組做爲參數
 * 返回值: 返回一個Promise實例
 * 只要有一個promise對象進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理(取決於哪個更快)
 */
Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
           promise.then(resolve, reject);
        });
    });
}

// 用於promise方法鏈時 捕獲前面onFulfilled/onRejected拋出的異常
Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

/**
 * 基於Promise實現Deferred的
 * Deferred和Promise的關係
 * - Deferred 擁有 Promise
 * - Deferred 具有對 Promise的狀態進行操做的特權方法(resolve reject)
 *
 *參考jQuery.Deferred
 *url: http://api.jquery.com/category/deferred-object/
 */
Promise.deferred = function() { // 延遲對象
    let defer = {};
    defer.promise = new Promise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}

/**
 * Promise/A+規範測試
 * npm i -g promises-aplus-tests
 * promises-aplus-tests Promise.js
 */

try {
  module.exports = Promise
} catch (e) {
}

複製代碼

async await

async函數是ES2017引入的新語法,是Generator 函數的語法糖,使得異步操做變得更加的方便。

來看一個axios發送請求獲取數據的栗子,假設每一次發送請求的param都依賴前一個ajax返回的response:

// 回調函數的形式,造成回調地獄
axios.get('url1', {
    params
}).then(data1 => {
    axios.get('url2', {
        params: data1
    }).then(data2 => {
        axios.get('url3', {
            params: data2
        }).then(data3 => {
            axios.get('url4', {
                params: data3
            }).then(data4 => {
                // ....一直回調
            })
        })
    })
});


// Promise解決回調地獄
const request = (url, params) => {
    return axios.post(url, params)
        .then(data => Promise.resolve(data))
        .catch(error => Promise.reject(error));
};

request(url1, params1)
    .then(data1 => {
        return request(url2, data1);
    }).then(data2 => {
        return request(url3, data2);
    }).then(data3 => {
        return request(url4, data3);
    }).catch(error => throw new Error(err));
    
複製代碼

async函數的語法規則比較好理解,語義比起Generator函數的*和yield,要更加地清晰,async表函數裏有異步操做,await表示緊跟在後面的表達式須要等待的結果。可是,async函數的難點在於誤處理機制。任何一個await語句後面的Promise對象變爲reject狀態,那麼整個async函數都會中執行。

錯誤處理,一種方法是將await後面的Promise對象再跟一個catch方法,處理前面可能出現的錯誤。

async function f() {
    await Promise.reject('出錯了')
        .catch(e => console.log(e));
    return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出錯了
// hello world
複製代碼

另一個方法,就是統一放在try..catch結構中

async function main() {
    try {
        const val1 = await firstStep();
        const val2 = await secondStep(val1);
        const val3 = await thirdStep(val1, val2);
    
        console.log('Final: ', val3);
    }
    catch (err) {
        console.error(err);
    }
}
複製代碼

總結:async函數能夠看做是多個異步操做,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖。


相關面試題

1.異步筆試題(頭條)

請寫出下面代碼的運行結果

async function async1() {
    console.log('async1 start'); // 4.輸出'async1 start'
    await async2(); // 5.遇到await時,會將後面的代碼執行一遍
    console.log('async1 end');//7.將await後面的代碼加入到microtask中的Promise隊列中,跳出async1函數來執行後面的代碼
    // 11.尋找到微任務隊列的第一個任務,輸出'async1 end'
}

async function async2() {
    console.log('async2'); // 6.輸出'async2'
}

console.log('script start'); // 1.打印'script start'

setTimeout(function() {
    console.log('setTimeout');
}, 0); // 2.加入宏任務隊列中

async1(); // 3.調用async1函數

new Promise(function(resolve) { // 8.Promise中的函數當即執行,輸出'promise1'
    console.log('promise1');
    resolve();
}).then(function() { // 9.promise中的then被分發到microtask的Promise隊列中
    console.log('promise2');
});

console.log('script end'); // 10.script任務繼續執行,輸出'scriptend',一個宏任務執行完畢,去檢查是否存在微任務
複製代碼

該題的本質,是考察setTimeout、promise、async await的實現及執行順序,以及JS的事件循環機制。

先來了解下宏任務(macrotask)和微任務(microtask):

宏任務

宏任務(macrotask):能夠理解成是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)

macrotask主要包括:

(1)script(總體代碼)
(2)setTimeout、setInterval
(3)I/O
(4)UI交互事件
(5)postMessage
(6)MessageChannel
(7)setImmediate(Node.js環境)
複製代碼
微任務

微任務(microtask),能夠理解是在當前task執行結束後當即執行的任務。也就是說,在當前task任務後,下一個task以前,在渲染以前。

microtask主要包括:

(1)promise
(2)process.nextTick(Node.js環境)
(3)MutationObserver
(4)Object.observer(廢棄)
複製代碼
2.setTimeout、Promise、Async/Await 的區別

1) setTimeout 屬於宏任務,setTimeout 的回調函數會放在宏任務隊列裏,等到執行棧清空之後執行

2) promise 屬於微任務,promise.then 裏的回調函數會放在微任務隊列裏,等宏任務裏的同步代碼執行完再執行

3) async 方法執行時,須要await時,會當即執行表達式,而後把表達式後面的代碼放到微任務隊列中,讓出執行棧讓同步代碼先執行,等當前宏任務完,再執行微任務隊列裏的任務

3.Async/Await 如何經過同步的方式實現異步(頭條、微醫)

經過 await 將異步代碼改形成同步代碼

看以下例子:

(async () => {
       var a = await A();
       var b = await B(a);
       var c = await C(b);
       var d = await D(c);
   })();
複製代碼
// Generator
   run(function*() {
       const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
       console.log(res1);
       const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
       console.log(res2);
   });

   // async/await
   const readFile = async ()=>{
       const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
       console.log(res1);
       const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
       console.log(res2);
       return 'done';
   }
   const res = readFile();

複製代碼
4.JS 異步解決方案的發展歷程以及優缺點。(滴滴、挖財、微醫、海康)

一、 回調函數(callback)

setTimeout(() => {
        // callback 函數體
    }, 1000)
複製代碼
ajax('XXX1', () => {
        // callback 函數體
        ajax('XXX2', () => {
            // callback 函數體
            ajax('XXX3', () => {
            // callback 函數體
            })
        })
    })
複製代碼

缺點:回調地獄,不能用 try catch 捕獲錯誤,不能 return

回調地獄有什麼問題?

  • 缺少順序性: 回調地獄致使的調試困難,和大腦的思惟方式不符
  • 嵌套函數存在耦合性,一旦有所改動,就會牽一髮而動全身
  • 嵌套函數過多的多話,很難處理錯誤

優勢:解決了同步阻塞的問題(只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。)

二、 Promise

Promise就是爲了解決callback的問題而產生的。

Promise 實現了鏈式調用,也就是說每次 then 後返回的都是一個全新 Promise,若是咱們在 then 中 return ,return 的結果會被 Promise.resolve() 包裝。

ajax('XXX1')
   .then(res => {
     // 操做邏輯
     return ajax('XXX2')
 }).then(res => {
     // 操做邏輯
     return ajax('XXX3')
 }).then(res => {
     // 操做邏輯
 })
複製代碼

優勢:解決了回調地獄的問題 缺點:沒法取消promise,錯誤須要經過回調函數來捕獲

三、 Generator

function *fetch() {
    yield ajax('XXX1', () => {})
    yield ajax('XXX2', () => {})
    yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
複製代碼

特色:能夠控制函數的執行,能夠配合 co 函數庫使用

  1. Async/await
async function test() {
    // 如下代碼沒有依賴性的話,徹底可使用 Promise.all 的方式
    // 若是有依賴性的話,其實就是解決回調地獄的例子了
    await fetch('XXX1')
    await fetch('XXX2')
    await fetch('XXX3')
}
複製代碼

優勢是:代碼清晰,不用像 Promise 寫一大堆 then 鏈,處理了回調地獄的問題。

缺點:await 將異步代碼改形成同步代碼,若是多個異步操做沒有依賴性而使用 await 會致使性能上的下降。

5.Promise 構造函數是同步執行仍是異步執行,那麼 then 方法呢?(微醫)

Promise 構造函數是同步執行的,而 then 是異步執行的,then 是屬於微任務。

這裏看一個例子:

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve(5);
  console.log(2);
}).then(val => {
  console.log(val);
});
promise.then(() => {
  console.log(3);
});
console.log(4);
setTimeout(function() {
  console.log(6);
});
複製代碼
6.模擬實現一個 Promise.finally

finally() 方法返回一個Promise。在promise結束時,不管結果是fulfilled或者是rejected,都會執行指定的函數。爲在Promise狀態被改變後會執行finally中的回調。 這避免了一樣的語句須要在then()和catch()中各寫一次的狀況。

先看promise實例finally的特性

  • 返回爲thenable對象,then函數中調用reject 則返回的以此爲拒因的promise
  • 返回爲thenable對象,執行then拋出錯誤, 則返回的以此錯誤爲拒因的promise
  • 返回爲thenable對象, 調用resolve,則返回狀態和值跟上層同樣的promise
  • finally回調返回rejected狀態的Promise, 則返回的以此爲拒因的promise
  • finally回調執行若是拋出錯誤,則返回的以拋錯爲拒因的promise
  • finally回調返回fulfilled狀態的promise,則返回狀態和值跟上層同樣的promise
  • 其餘狀況,則返回狀態和值跟上層同樣的promise

關於實現,下面第一個是基於以前resolvePromise函數實現的, 第二個是阮一峯ES6文章中的實現
阮一峯 ECMAScript6入門 - Promise.prototype.then)

Promise.prototype._finally = function (cb) {
    const that = this;
    function resolvePromise(x, resolve, reject) {
        if (x instanceof Promise) {
            x.then(() => that.then(resolve, reject), reject);
        } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) {
            try { // 是不是thenable對象(具備then方法的對象/函數)
                let then = x.then;
                if (typeof then === 'function') {
                    then.call(x, () => that.then(resolve, reject), reject);
                } else { // 說明是一個普通對象/函數
                    that.then(resolve, reject);
                }
            } catch (e) {
                reject(e);
            }
        } else {
            that.then(resolve, reject);
        }
    }
    return new Promise((resolve, reject) => {
        try {
            var v = cb();
            resolvePromise(v, resolve, reject)
        } catch (err) {
            reject(err);
        }
    })
}

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};
複製代碼
7.介紹下 Promise.all 使用、原理實現及錯誤處理

Promise.all使用:

const p = Promise.all([p1, p2, p3]);
p.then(res => /* to do */)
 .catch(err => reject(err));
複製代碼

Promise.all特性:

  • 對Promise實例進行並行處理,接收一個promise對象組成的數組做爲參數
  • 當這個數組裏的全部promise對象所有進入FulFilled狀態時,一次性resolve一個promise實例對象,
  • 當數組裏只要存在一個reject狀態的promise對象,則返回失敗的信息
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let values = [];
        let count = 0;
        promises.forEach((promise, index) => {
            promise.then(value => {
                values[index] = value;
                count++;
                if (count === promises.length) resolve(values)
            }, reject)
        })
    }
}
複製代碼

Promise.all錯誤處理

  • 1.所有改成串行調用(失去了node併發優點)
  • 2.當promise捕獲到error的時候,代碼吃掉這個異常,返回resolve,約定特殊格式表示這個調用成功了
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1)
    }, 0)
});

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2)
    }, 200)
});

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        try {
            console.log(ABC.efd);
        } catch (err) {
            resolve('error') // 這裏是關鍵
        }
    },100)
});

Promise.all([p1, p2, p3]).then((results) => {
    console.log('success');
    console.log(results);
}).catch((error) => {
    console.log('err');
    console.log(error);
})

// Output: 'success' [1, 2. 'error']
複製代碼
8.設計並實現 Promise.race()
const p = Promise.race([p1, p2, p3]);
複製代碼

Promise.race()特性:

  • 與all同樣,參數爲多個Promise實例對象組成的數組
  • 與all不同的是,Promise.race是隻要實例數組中有一個實例率先改變狀態,就會把這個率先改變狀態的promise的返回值傳遞給p的回調函數。 Promise.race的實現
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise) => {
            promise.then(resolve, reject);
        })
    })
}
複製代碼

參考資料

木易楊前端進階 每日壹題

阮一峯 ECMAScript6入門 Promise對象

Promise MDN

瀏覽器的Tasks、microtasks、queues和schedules

阮一峯 ECMAScript6入門 async函數

混沌傳奇 從零一步一步實現一個完整版的Promise

Promise A+ 規範

相關文章
相關標籤/搜索