一文了解Promise使用與實現

前言

Promise 作為一個前端必備技能,不管是從項目應用還是面試,都應該對其有所了解與使用。

常常遇到的面試五連問:

  • 說說你對 Promise 理解?
  • Promise 的出現解決了什麼問題?
  • Promise 有哪些狀態?
  • 你用過 Promise 哪些方法?
  • 如何實現一個 Promise ?

什麼是 Promise?

Promise 是非同步編程的一種解決方案:從語法上講,promise 是一個對象,從它可以獲取非同步操作的消息;從本意上講,它是承諾,承諾它過一段時間會給你一個結果。

Promise 有三種狀態:pending(等待態),fulfiled(成功態),rejected(失敗態);狀態一旦改變,就不會再變。創造 Promise 實例後,它會立即執行。

一般來說我們會碰到的回調嵌套都不會很多,一般就一到兩級,但是某些情況下,回調嵌套很多時,程式碼就會非常繁瑣,會給我們的編程帶來很多的麻煩,這種情況俗稱——回調地獄。

這時候我們的Promise 就應運而生、粉墨登場了。

Promise 的基本使用

Promise 是一個構造函數,自己身上有 allrejectresolve 這幾個眼熟的方法,原型上有 thencatch 等同樣很眼熟的方法。

首先創建一個 new Promise 實例

let p = new Promise((resolve, reject) => {
    // 可以做一些非同步操作,例如發送AJAX請求,這裡使用 setTimeout 模擬
    setTimeout(() => {
        let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
        if (num <= 5) {
            resolve('成功');
        } else {
            reject('失敗');
        }
    }, 2000);
});

Promise 的構造函數接收一個參數:函數,並且這個函數需要傳入兩個參數:

resolve:非同步操作執行成功後的回調函數;

reject:非同步操作執行失敗後的回調函數;

then 鏈式操作的用法

p.then((data) => {
    console.log('resolve', data); // 'resolve', '成功'
}, (err) => {
    console.log('reject', err); // 'reject', '失敗'
})

then 中傳了兩個參數,then 方法可以接受兩個參數,第一個對應 resolve 的回調,第二個對應reject 的回調。所以我們能夠分別拿到他們傳過來的數據。多次運行這段程式碼,你會隨機得到兩種結果。

catch 的用法

我們知道Promise 對象除了then 方法,還有一個catch 方法,它是做什麼用的呢?其實它和then 的第二個參數一樣,用來指定reject 的回調。用法是這樣:

p.then((data) => {
    console.log('resolve', data); // 'resolve', '成功'
}).catch((err) => {
    console.log('reject', err);   // 'reject', '失敗'
});

效果和寫在 then 的第二個參數裡面一樣。不過它還有另外一個作用:在執行 resolve 的回調(也就是上面then 中的第一個參數)時,如果拋出異常了(程式碼出錯了),那麼並不會報錯卡死js,而是會進到這個 catch 方法中。請看下面的程式碼:

p.then((data) => {
    console.log('resolve', data);
    console.log(test);
}).catch((err) => {
    console.log('reject', err);
});

// 當成功時先後列印
resolve 成功
reject ReferenceError: test is not defined

resolve 的回調中,我們 console.log(test),而 test 這個變數是沒有被定義的。如果我們不用 Promise,程式碼運行到這裡就直接在控制台報錯了,不往下運行了,也就是說進到catch方法裡面去了,而且把錯誤原因傳到了 reject 參數中。即便是有錯誤的程式碼也不會報錯了,這與我們的 try/catch 語句有相同的功能。

finally 的用法

finally 方法返回一個 Promise。在 promise 結束時,無論結果是 fulfilled 或者是 rejected ,都會執行指定的回調函數。這為在 Promise 是否成功完成後都需要執行的程式碼提供了一種方式。

這避免了同樣的語句需要在 thencatch 中各寫一次的情況。

// 載入 loading

let isLoading = true;

// 假裝模擬AJAX請求
function myRequest(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
            if (num <= 5) {
                resolve('成功');
            } else {
                reject('失敗');
            }
        }, 1000);
    })
}

myRequest().then(function(data) { console.log(data); })
  .catch(function(error) { console.log(error); })
  .finally(function() { 
      // 關閉loading
      isLoading = false;
      console.log('finally');
   });

resolve 的用法

Promise.resolve(value) 方法返回一個以給定值解析後的 Promise 對象。如果這個值是一個 promise ,那麼將返回這個 promise ;如果這個值是 thenable(即帶有”then” 方法),返回的promise會「跟隨」這個 thenable 的對象,採用它的最終狀態;否則返回的promise將以此值完成。此函數將類promise對象的多層嵌套展平。

// 示例 1 基本使用

const p = Promise.resolve("Success");

p.then(function(value) {
  console.log(value); // "Success"
}, function(value) {
  // 不會被調用
});

// 示例 2 resolve 一個數組

var p = Promise.resolve([1,2,3]);
p.then(function(arr) {
  console.log(arr[0]); // 1
});

// 示例 3 resolve 另一個Promise

let original = Promise.resolve(33);
let cast = Promise.resolve(original);
cast.then(function(value) {
  console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

/*
*  列印順序如下,這裡有一個同步非同步先後執行的區別
*  original === cast ? true
*  value: 33
*/



// 示例 4 resolve thenable 並拋出錯誤

// Resolve一個thenable對象

const p1 = Promise.resolve({
  then: function(onFulfill, onReject) { onFulfill("fulfilled!"); }
});
console.log(p1 instanceof Promise) // true, 這是一個Promise對象

p1.then(function(v) {
    console.log(v); // 輸出"fulfilled!"
  }, function(e) {
    // 不會被調用
});

// Thenable在callback之前拋出異常

// Promise rejects

const thenable = { then: function(resolve) {
  throw new TypeError("Throwing");
  resolve("Resolving");
}};

const p2 = Promise.resolve(thenable);

p2.then(function(v) {
  // 不會被調用
}, function(e) {
  console.log(e); // TypeError: Throwing
});

// Thenable在callback之後拋出異常

// Promise resolves

const thenable = { then: function(resolve) {
  resolve("Resolving");
  throw new TypeError("Throwing");
}};

const p3 = Promise.resolve(thenable);

p3.then(function(v) {
  console.log(v); // 輸出"Resolving"
}, function(e) {
  // 不會被調用
});

reject 的用法

Promise.reject()方法返回一個帶有拒絕原因的Promise對象。

Promise.reject(new Error('fail')).then(function() {
  // not called
}, function(error) {
  console.error(error); // Stacktrace
});

all 的用法

誰跑的慢,以誰為準執行回調。all 接收一個數組參數,裡面的值最終都算返回 Promise 對象。

Promise.all 方法提供了並行執行非同步操作的能力,並且在所有非同步操作執行完後才執行回調。看下面的例子:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        let num = Math.ceil(Math.random() * 10); // 生成 1-10 隨機數
        if (num <= 5) {
            resolve('成功');
        } else {
            reject('失敗');
        }
    }, 1000);
});

let pAll = Promise.all([p, p, p]);

pAll.then((data) => {
    console.log(data); // 成功時列印: ['成功', '成功', '成功']
}, (err) => {
    console.log(errs); // 只要有一個失敗,就會返回當前失敗的結果。 '失敗'
})

有了all,你就可以並行執行多個非同步操作,並且在一個回調中處理所有的返回數據,是不是很酷?有一個場景是很適合用這個的,一些遊戲類的素材比較多的應用,打開網頁時,預先載入需要用到的各種資源如圖片、flash以及各種靜態文件。所有的都載入完後,我們再進行頁面的初始化。在這裡可以解決時間性能的問題,我們不需要在把每個非同步過程同步出來。

all 缺點就是只要有一個任務失敗就會都失敗。

allSettled 的用法

Promise.allSettled ****方法返回一個在所有給定的 promise 都已經fulfilledrejected後的 promise,並帶有一個對象數組,每個對象表示對應的 promise 結果。

當您有多個彼此不依賴的非同步任務成功完成時,或者您總是想知道每個promise的結果時,通常使用它。

相比之下,Promise.all 更適合彼此相互依賴或者在其中任何一個reject時立即結束。

const p1 = Promise.resolve(3);
const p2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));

Promise.allSettled([p1, p2]).
  then((results) => results.forEach((result) => console.log(result.status)));

// 執行後列印
// "fulfilled"
// "rejected"

any 的用法

Promise.any 接收一個 promise 可迭代對象,只要其中的一個 promise 成功,就返回那個已經成功的 promise 。如果可迭代對象中沒有一個 promise 成功(即所有的 promises 都失敗/拒絕),就返回一個失敗的 promiseAggregateError 類型的實例,它是 Error 的一個子類,用於把單一的錯誤集合在一起。本質上,這個方法和 Promise.all 是相反的。

const pErr = new Promise((resolve, reject) => {
  reject("總是失敗");
});

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最終完成");
});

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);  // '很快完成'
})

race 的用法

Promise.race ****方法返回一個 promise,一旦迭代器中的某個 promise 解決或拒絕,返回的 promise就會解決或拒絕。也可理解 誰跑的快,以誰為準執行回調。

race 的使用場景:比如我們可以用race 給某個非同步請求設置超時時間,並且在超時後執行相應的操作,程式碼如下:

    // 假設請加某個圖片資源
    function requestImg() {
        return new Promise((resolve, reject) => {
            let img = new Image();
            img.onload = function() {
                resolve(img);
            }
            // 嘗試輸入假的和真的鏈接
            img.src = '**';
        })
    }
    // 延時函數,用於給請求計時
    function timeout() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('請求超時');
            }, 5000)
        })
    }

    Promise.race([requestImg(), timeout()]).then((data) => {
        console.log(data); // 成功時 img
    }).catch((err) => {
        console.log(err);  // 失敗時 "請求超時"
    })
    

如何實現一個Promise

1、創建一個 MyPromise 類,傳入 executor(執行器)並添加 resolve 和 reject 方法


class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    // 並傳入resolve和reject方法
    executor(this.resolve, this.reject) 
  }
  
  // 更改成功後的狀態
  resolve = () => {}
  // 更改失敗後的狀態
  reject = () => {}
}

2、添加狀態和 resolve、reject 事件處理

// 定義三種狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  constructor(executor){
    executor(this.resolve, this.reject)
  }

  // 儲存狀態,初始值是 pending
  status = PENDING;

  // 成功之後的值
  value = null;
  
  // 失敗之後的原因
  reason = null;

  // 更改成功後的狀態
  resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED;
      this.value = value;
    }
  }

  // 更改失敗後的狀態
  reject = (reason) => {
    if (this.status === PENDING) {
      this.status = REJECTED;
      this.reason = reason;
    }
  }
}

3、.then 的實現

then(onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    onFulfilled(this.value); // 調用成功回調,並且把值返回
  } else if (this.status === REJECTED) {
    onRejected(this.reason); // 調用失敗回調,並且把原因返回
  }
}

上面三步已經簡單實現了一個 Promise我們先來調用試試:

const promise = new MyPromise((resolve, reject) => {
   resolve('success')
   reject('err')
})

promise.then(value => {
  console.log('resolve', value)
}, reason => {
  console.log('reject', reason)
})

// 列印結果:resolve success

經過測試發現如果使用非同步調用就會出現順序錯誤那麼我們怎麼解決呢?

4、實現非同步處理

// 定義三種狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
    constructor(executor) {
        executor(this.resolve, this.reject)
    }

    // 成功存放的數組
    onResolvedCallbacks = [];
    // 失敗存放法數組
    onRejectedCallbacks = [];
    // 儲存狀態,初始值是 pending
    status = PENDING;
    // 成功之後的值
    value = null;
    // 失敗之後的原因
    reason = null;

    // 更改成功後的狀態
    resolve = (value) => {
        if (this.status === PENDING) {
            this.status = FULFILLED;
            this.value = value;
            this.onRejectedCallbacks.forEach((fn) => fn()); // 調用成功非同步回調事件
        }
    }

    // 更改失敗後的狀態
    reject = (reason) => {
        if (this.status === PENDING) {
            this.status = REJECTED;
            this.reason = reason;
            this.onRejectedCallback.forEach((fn) => fn());  // 調用失敗非同步回調事件
        }
    }
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value); //調用成功回調,並且把值返回
        } else if (this.status === REJECTED) {
            onRejected(this.reason); // 調用失敗回調,並且把原因返回
        } else if (this.status === PENDING) {
            // onFulfilled傳入到成功數組
            this.onResolvedCallbacks.push(() => {
                onFulfilled(this.value);
            })
            // onRejected傳入到失敗數組
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
        }
    }
}

修改後通過調用非同步測試沒有

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 2000); 
})

promise.then(value => {
  console.log('resolve', value)
}, reason => {
  console.log('reject', reason)
})

// 等待 2s 輸出 resolve success

但是如果使用鏈式調用 .then 就會發現有問題,而原生的 Promise 是支援的。 那麼我們如何支援呢?

鏈式調用示例:

const promise = new MyPromise((resolve, reject) => {
  resolve('success')
})

function other () {
  return new MyPromise((resolve, reject) =>{
    resolve('other')
  })
}
promise.then(value => {
  console.log(1)
  console.log('resolve', value)
  return other()
}).then(value => {
  console.log(2)
  console.log('resolve', value)
})
// 第二次 .then 將會失敗

5、實現 .then 鏈式調用

class MyPromise {
    ...

    then(onFulfilled, onRejected) {
        // 為了鏈式調用這裡直接創建一個 MyPromise,並 return 出去
        return new MyPromise((resolve, reject) => {
            if (this.status === FULFILLED) {
                const x = onFulfilled(this.value);
                resolvePromise(x, resolve, reject);
            } else if (this.status === REJECTED) {
                onRejected(this.reason);
            } else if (this.status === PENDING) {
                this.onFulfilledCallbacks.push(onFulfilled);
                this.onRejectedCallbacks.push(onRejected);
            }
        })
    }
}


function resolvePromise(x, resolve, reject) {
    // 判斷x是不是 MyPromise 實例對象
    if (x instanceof MyPromise) {
        // 執行 x,調用 then 方法,目的是將其狀態變為 fulfilled 或者 rejected
        x.then(resolve, reject)
    } else {
        resolve(x)
    }
}

6、也可以加入 try/catch 容錯

    ...
    constructor(executor) {
        try {
            executor(this.resolve, this.reject)
        } catch(error) {
            this.reject(error)
        }
    }

最後

本文就先到這裡了,後續有時間再補充 .alL.any 等其他方法的實現。

相關推薦

//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
//developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

作者:雨中愚
鏈接://juejin.cn/post/6995923016643248165
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。