js--promise、async 和 await 相關知識總結

丶Serendipity丶發表於2022-05-15

前言

  promise 是前端開發人員必須掌握的知識點,本文來總結一下相關學習筆記。

正文

  1、什麼是prommise,promise 解決了什麼問題

  a、promise 是什麼

  Promise 是承諾的意思,承諾它過一段時間會給你一個結果。Promise 是一種解決非同步程式設計的方案,相比回撥函式和事件更合理和更強大。從語法上講,promise 是一個物件,從它可以獲取非同步操作的訊息;

  promise 有三種狀態:pending 初始狀態也叫等待狀態,fulfiled成功狀態,rejected 失敗狀態;狀態一旦改變,就不會再變。創造 promise例項後,它會立即執行。需要注意,promise 的狀態是不可逆的,一旦狀態由 pending 變為 fulfiled 或者reject 狀態,意味著已經產生了結果,同樣,轉為成功狀態會有成功的結果,轉為失敗狀態會返回失敗的原因。

  promise 作為建構函式,接收兩個引數,分別是成功和失敗的回撥函式。

  b、promise 解決了什麼問題

  我們先來看如下程式碼,並不陌生

  setTimeout(function () {
    console.log("開始執行");
  }, 3000);

  上面的程式碼,粗略的可以認為在 3 秒後,程式輸出開始執行,但是如果業務比較複雜,我們想在3秒後輸出開始執行,再隔 3 秒列印一次第二次執行呢?接著在隔 3 秒列印第三次執行,程式碼會這樣寫:

  setTimeout(function () {
    console.log("開始執行");
    setTimeout(function () {
      console.log("第二次執行");
      setTimeout(function () {
        console.log("第三次執行");
      }, 3000);
    }, 3000);
  }, 3000);

  再看上面的程式碼,如果後面的需求再次優化,需要類似的列印第 4,5,6 次呢?我們的程式碼還是這樣一層層巢狀起來嗎? 這樣多層函式之間互相巢狀,就產生了回撥地獄的問題,這樣寫程式碼有個很大的缺點:(1)程式碼耦合行太強,牽一髮而動全身,可維護性很差,同樣,大量冗餘的程式碼互相巢狀,可讀性很差。因此,為了解決回撥地獄的問題,ES6 提出了 Promise。通過 promise 將上面的程式碼改裝一下將顯的程式碼優雅很多:

    function sleep(second) {
      return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), second * 1000);
      });
    }
    sleep(3)
      .then(() => {
        console.log("開始執行");
        return sleep(3);
      })
      .then(() => {
        console.log("第二次執行");
        return sleep(3);
      })
      .then(() => {
        console.log("第三次執行");
      });

  2、ES6 中 promise 的使用

  1)then 鏈式呼叫

  從表面上看,Promise只是能夠簡化層層回撥的寫法,而實質上,Promise 的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞 callback 函式要簡單、靈活的多。所以使用 Promise 的正確場景是這樣的:

 

    p.then((data) => {
      console.log(data);
    })
      .then((data) => {
        console.log(data);
      })
      .then((data) => {
        console.log(data);
      });

 

  then 是例項狀態發生改變時的回撥函式,第一個引數是 resolved 狀態的回撥函式,第二個引數是 rejected 狀態的回撥函式,then方法返回的是一個新的Promise例項,也就是promise能鏈式書寫的原因。預設常寫第一個引數即可,reject 狀態的回撥可以通過 catch 來捕獲異常。

 

  2)catch 方法用來指定 promise 例項狀態變為 rejected 的捕獲

  catch 捕獲reject狀態的回撥,相當於 then中的第二個引數,一般寫法如下:

    p.then((data) => {
      console.log("resolved", data);
    }).catch((err) => {
      console.log("rejected", err);
    });

  也就是說進到catch方法裡面去了,而且把錯誤原因傳到了reason引數中。即便是有錯誤的程式碼也不會報錯了,這與我們的try/catch語句有相同的功能。

  3)all 方法將多個 promise 例項包裝成一個新的 promise 例項(誰跑的慢,以誰為準執行回撥)

  Promise.all 方法接收一個陣列(可迭代物件)作為引數,並且陣列中的每個元素都是 Promise 例項,最終返回結果也為一個 Promise  物件,例如:

const p = Promise.all([p1, p2, p3]),例項p的狀態由p1、p2、p3決定,分為兩種:

  只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式;

  只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式;

  常見的寫法如下:

    let Promise1 = new Promise(function (resolve, reject) {});
    let Promise2 = new Promise(function (resolve, reject) {});
    let Promise3 = new Promise(function (resolve, reject) {});
    
    let p = Promise.all([Promise1, Promise2, Promise3]);
    
    p.then(
      (res) => {
        // 三者都成功則成功,成功後處理
      },
      (err) => {
        // 三者只要有失敗的就返回失敗,失敗後處理
      }
    );

  有了all 方法,我們就可以並行執行多個非同步操作,並且在一個回撥中處理所有的返回資料,比如開發中開啟網頁時,預先載入需要用到的各種資源如圖片、flash 以及各種靜態檔案,所有的都載入完後,我們再進行頁面的初始化。

  (4)race 方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項(誰跑的快,以誰為準執行回撥)

  const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變,率先改變的 Promise 例項的返回值則傳遞給p的回撥函式。

  使用場景:比如,我們在頁面載入的時候,需要請求後端獲取某個圖片的URL,這裡我們可以設定請求的超時時間,當在設定的時間內後端介面沒有返回時,頁面給出請求超時提示。程式碼如下:

    //請求某個圖片資源
        function requestImg() {
            var p = new Promise((resolve, reject) => {
                var img = new Image();
                img.onload = function () {
                    resolve(img);
                }
                img.src = '圖片的路徑';
            });
            return p;
        }
        //延時函式,用於給請求計時
        function timeout() {
            var p = new Promise((resolve, reject) => {
                setTimeout(() => {
                    reject('圖片請求超時');
                }, 5000);
            });
            return p;
        }
        Promise.race([requestImg(), timeout()]).then((data) => {
            console.log(data);
        }).catch((err) => {
            console.log(err);
        });

  3、promise 的缺點

  1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消  

  2)如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反映到外部

  3)當處於 pending(等待)狀態時,無法得知目前進展到哪一個階段,是剛剛開始還是即將完成

  4、手動實現 Promise

        function resolvePromise(promise2, x, resolve, reject) {
            //判斷x是不是promise
            //規範中規定:我們允許別人亂寫,這個程式碼可以實現我們的promise和別人的promise 進行互動
            if (promise2 === x) {//不能自己等待自己完成
                return reject(new TypeError('迴圈引用'));
            };
            // x是除了null以外的物件或者函式
            if (x != null && (typeof x === 'object' || typeof x === 'function')) {
                let called;//防止成功後呼叫失敗
                try {//防止取then是出現異常  object.defineProperty
                    let then = x.then;//取x的then方法 {then:{}}
                    if (typeof then === 'function') {//如果then是函式就認為他是promise
                        //call第一個引數是this,後面的是成功的回撥和失敗的回撥
                        then.call(x, y => {//如果Y是promise就繼續遞迴promise
                            if (called) return;
                            called = true;
                            resolvePromise(promise2, y, resolve, reject)
                        }, r => { //只要失敗了就失敗了
                            if (called) return;
                            called = true;
                            reject(r);
                        });
                    } else {//then是一個普通物件,就直接成功即可
                        resolve(x);
                    }
                } catch (e) {
                    if (called) return;
                    called = true;
                    reject(e)
                }
            } else {//x = 123 x就是一個普通值 作為下個then成功的引數
                resolve(x)
            }

        }

        class Promise {
            constructor(executor) {
                //預設狀態是等待狀態
                this.status = 'panding';
                this.value = undefined;
                this.reason = undefined;
                //存放成功的回撥
                this.onResolvedCallbacks = [];
                //存放失敗的回撥
                this.onRejectedCallbacks = [];
                let resolve = (data) => {//this指的是例項
                    if (this.status === 'pending') {
                        this.value = data;
                        this.status = "resolved";
                        this.onResolvedCallbacks.forEach(fn => fn());
                    }

                }
                let reject = (reason) => {
                    if (this.status === 'pending') {
                        this.reason = reason;
                        this.status = 'rejected';
                        this.onRejectedCallbacks.forEach(fn => fn());
                    }
                }
                try {//執行時可能會發生異常
                    executor(resolve, reject);
                } catch (e) {
                    reject(e);//promise失敗了
                }

            }
            then(onFuiFilled, onRejected) {
                //防止值得穿透 
                onFuiFilled = typeof onFuiFilled === 'function' ? onFuiFilled : y => y;
                onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; }
                let promise2;//作為下一次then方法的promise
                if (this.status === 'resolved') {
                    promise2 = new Promise((resolve, reject) => {
                        setTimeout(() => {
                            try {
                                //成功的邏輯 失敗的邏輯
                                let x = onFuiFilled(this.value);
                                //看x是不是promise 如果是promise取他的結果 作為promise2成功的的結果
                                //如果返回一個普通值,作為promise2成功的結果
                                //resolvePromise可以解析x和promise2之間的關係
                                //在resolvePromise中傳入四個引數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。
                                resolvePromise(promise2, x, resolve, reject)
                            } catch (e) {
                                reject(e);
                            }
                        }, 0)
                    });
                }
                if (this.status === 'rejected') {
                    promise2 = new Promise((resolve, reject) => {
                        setTimeout(() => {
                            try {
                                let x = onRejected(this.reason);
                                //在resolvePromise中傳入四個引數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。
                                resolvePromise(promise2, x, resolve, reject)
                            } catch (e) {
                                reject(e);
                            }
                        }, 0)

                    });
                }
                //當前既沒有完成也沒有失敗
                if (this.status === 'pending') {
                    promise2 = new Promise((resolve, reject) => {
                        //把成功的函式一個個存放到成功回撥函式陣列中
                        this.onResolvedCallbacks.push(() => {
                            setTimeout(() => {
                                try {
                                    let x = onFuiFilled(this.value);
                                    resolvePromise(promise2, x, resolve, reject);
                                } catch (e) {
                                    reject(e);
                                }
                            }, 0)
                        });
                        //把失敗的函式一個個存放到失敗回撥函式陣列中
                        this.onRejectedCallbacks.push(() => {
                            setTimeout(() => {
                                try {
                                    let x = onRejected(this.reason);
                                    resolvePromise(promise2, x, resolve, reject)
                                } catch (e) {
                                    reject(e)
                                }
                            }, 0)
                        })
                    })
                }
                return promise2;//呼叫then後返回一個新的promise
            }
            catch(onRejected) {
                // catch 方法就是then方法沒有成功的簡寫
                return this.then(null, onRejected);
            }
        }
        Promise.all = function (promises) {
            //promises是一個promise的陣列
            return new Promise(function (resolve, reject) {
                let arr = []; //arr是最終返回值的結果
                let i = 0; // 表示成功了多少次
                function processData(index, data) {
                    arr[index] = data;
                    if (++i === promises.length) {
                        resolve(arr);
                    }
                }
                for (let i = 0; i < promises.length; i++) {
                    promises[i].then(function (data) {
                        processData(i, data)
                    }, reject)
                }
            })
        }
        // 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了
        Promise.race = function (promises) {
            return new Promise((resolve, reject) => {
                for (var i = 0; i < promises.length; i++) {
                    promises[i].then(resolve, reject)
                }
            })
        }
        // 生成一個成功的promise
        Promise.resolve = function (value) {
            return new Promise((resolve, reject) => resolve(value);
        }
        // 生成一個失敗的promise
        Promise.reject = function (reason) {
            return new Promise((resolve, reject) => reject(reason));
        }
        module.exports = Promise;

  5、async 和 await相關

  async/await 是 ES7 提出的基於 Promise 的解決非同步的最終方案。

  async 就是 generation 和 promise 的語法糖,async 就是將 generator的*換成 async,將 yiled 換成 await函式前必須加一個 async,非同步操作方法前加一個 await 關鍵字,意思就是等一下,執行完了再繼續走,注意:await 只能在 async 函式中執行,否則會報錯,Promise 如果返回的是一個錯誤的結果,如果沒有做異常處理,就會報錯,所以用 try..catch 捕獲一下異常就可以了。

  async

async是一個加在函式前的修飾符,被async定義的函式會預設返回一個Promise物件resolve的值。因此對async函式可以直接then,返回值就是then方法傳入的函式。使用如下:

        async function fun() {
            console.log(1);
            return 1;
        }
        fun().then(val => {
            console.log(val) // 1,1
        })

  await

  await 也是一個修飾符,只能放在async定義的函式內。可以理解為等待。await 修飾的如果是Promise物件:可以獲取Promise中返回的內容(resolve或reject的引數),且取到值後語句才會往下執行;如果不是Promise物件:把這個非promise的東西當做await表示式的結果。使用如下:

        async function fun() {
            let a = await new Promise((resolve, reject) => {
                setTimeout(function () {
                    resolve('setTimeout promise')
                }, 3000)
            })
            let b = await "表示式";
            let c = await function () {
                return '函式表示式'
            }()
            console.log(a, b, c)
        }
        fun(); // 3秒後輸出:"setTimeout promise" "表示式" "函式表示式"

寫在最後

   以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。

 

誰跑的慢,以誰為準執行回撥

相關文章