JavaScript Promise物件

雲崖先生發表於2020-08-14

事件迴圈

  JavaScript是一門單執行緒的程式語言,所以沒有併發並行等特性。

  為了協調事件、使用者互動、指令碼、UI 渲染和網路處理等行為,防止主執行緒的不阻塞,(事件迴圈)Event Loop的方案應用而生。

  JavaScript處理任務是在等待任務、執行任務 、休眠等待新任務中不斷迴圈中,也稱這種機制為事件迴圈。

  主執行緒中的任務執行完後,才執行任務佇列中的任務

  有新任務到來時會將其放入佇列,採取先進先執行的策略執行佇列中的任務

  比如多個 setTimeout 同時到時間了,就要依次執行

  任務包括 script(整體程式碼)、 setTimeoutsetInterval、DOM渲染、DOM事件、PromiseXMLHTTPREQUEST

image-20200813235308171

任務詳解

任務分類

  任務大致分為以下三種:

  主執行緒任務

  應放入巨集佇列中的任務

  應放入微佇列中的任務

放入巨集佇列中的任務  
# 瀏覽器 Node
setTimeout
setInterval
setImmediate x
requestAnimationFrame x
放入微佇列中的任務  
# 瀏覽器 Node
process.nextTick x
MutationObserver x
Promise.then catch finally

執行順序

  根據任務的不同,執行順序也有所不同:

  1.主執行緒任務

  2.微佇列任務

  3.巨集佇列任務

<script>

        "use strict";

        new Promise(resolve => {
                console.log("主執行緒任務執行 1...")
                resolve();
        }).then(_ => {
                console.log("微佇列任務執行 7...");
        });

        console.log("主執行緒任務執行 2...");

        setTimeout(() => {
                console.log("巨集佇列任務執行 9...");
        }, 1);

        console.log("主執行緒任務執行 3...");

        new Promise(resolve => {
                console.log("主執行緒任務執行 4...")
                resolve();
        }).then(_ => {
                console.log("微佇列任務執行 8...");
        });

        console.log("主執行緒任務執行 5...");
        console.log("主執行緒任務執行 6...");
/*
        
        主執行緒任務執行 1...
        主執行緒任務執行 2...
        主執行緒任務執行 3...
        主執行緒任務執行 4...
        主執行緒任務執行 5...
        主執行緒任務執行 6...
        微佇列任務執行 7...
        微佇列任務執行 8...
        巨集佇列任務執行 9...
        
        */

</script>

作用體現

  使用Promise能讓程式碼變得更易閱讀,方便後期維護。

  特別是在回撥函式巢狀上,更應該使用Promise來書寫程式碼。

巢狀問題

  以下示例將展示通過Js來使得<div>標籤形態在不同時刻發生變化。

  程式碼邏輯雖然清晰但是定時器回撥函式巢狀太過複雜,閱讀體驗較差。

<!DOCTYPE html>
<html lang="en">

<head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
                div {
                        width: 100px;
                        height: 100px;
                        background-color: red;
                        transition: 1s;
                }

                button {

                        margin-top: 20px;
                }
        </style>
</head>

<body>
        <div></div>
        <button>點我</button>
</body>
<script>

        "use strict";

        document.querySelector("button").addEventListener("click", () => {

                let div = document.querySelector("div");
                div.style.backgroundColor = "blue";
                setTimeout(() => {
                        div.style.width = "50px";
                        setTimeout(() => {
                                div.style.transform = "translate(100px)";
                                setTimeout(() => {
                                        div.style.width = "100px";
                                        div.style.backgroundColor = "red";
                                        setTimeout(() => {
                                                div.style.backgroundColor = "yellow";
                                        },1000);

                                }, 1000);

                        }, 1000);

                }, 1000);
        });

</script>

</html>

嘗試解決

  使用Promise來解決該問題。

  這裡看不懂沒關係,下面會慢慢進行剖析,只是感受一下是不是巢狀沒那麼嚴重了看起來好看多了。

<!DOCTYPE html>
<html lang="en">

<head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
                div {
                        width: 100px;
                        height: 100px;
                        background-color: red;
                        transition: 1s;
                }

                button {

                        margin-top: 20px;
                }
        </style>
</head>

<body>
        <div></div>
        <button>點我</button>
</body>
<script>

        "use strict";

        function chain(callback, time=1000) {

                return new Promise(function (resolve, reject) {
                        setTimeout(() => {
                                let res = callback();
                                resolve(res);
                        }, time);
                });
        }

        document.querySelector("button").addEventListener("click", () => {
                new Promise(function (resolve, reject) {

                        let div = document.querySelector("div");
                        div.style.backgroundColor = "blue";
                        resolve(div);

                }).then(div => {

                        return chain(() => {
                                div.style.width = "50px";
                                return div;
                        });

                }).then(div => {


                        return chain(() => {
                                div.style.transform = "translate(100px)";
                                return div;
                        });


                }).then(div => {

                        return chain(() => {
                                div.style.width = "100px";
                                div.style.backgroundColor = "red";
                                return div;
                        })


                }).then(div => {

                        return chain(() => {
                                div.style.backgroundColor = "yellow";
                                return div;
                        })


                })
        });

</script>

Promise

  JavaScript 中存在很多非同步操作,Promise 將非同步操作佇列化,按照期望的順序執行,返回符合預期的結果。

  可以通過鏈式呼叫多個 Promise 達到我們的目的,如同上面示例一樣會讓程式碼可讀性大幅度提升。

宣告狀態

  每一個Promise物件都接收一個函式,該函式需要提供兩個引數,分別是resolve以及reject,代表當前函式中的任務成功與失敗,這是屬於執行緒任務的,所以會優先執行。

  此外,每一個Promise物件都具有三種狀態,分別是pendingfulfilledrejected

  當一個Promise物件狀態改變過後,將不能再次改變。

  pending 指初始等待狀態,初始化 promise 時的狀態

  resolve 指已經解決,將 promise 狀態設定為fulfilled

  reject 指拒絕處理或未解決,將 promise 狀態設定為rejected

image-20200814140550236

  當沒有使用 resolvereject 更改狀態時,狀態為 pending

<script>
        "use strict";

        let p1 = new Promise(function (resolve, reject) { });
        console.log(p1);  // Promise {<pending>}

</script>

  使用resolve修改狀態後,狀態為fulfilled

<script>
        "use strict";

        let p1 = new Promise(function (resolve, reject) {
                resolve("已解決");
        });
        console.log(p1);  // Promise {<fulfilled>: "已解決"}

</script>

  使用reject修改狀態後,狀態為rejected

<script>
        "use strict";

        let p1 = new Promise(function (resolve, reject) {
                reject("未解決");
        });
        console.log(p1);  // Promise {<rejected>: "未解決"}

</script>

then

  在一個Promise物件狀態為resolvereject時,可以緊跟then方法,該方法可接收兩個個函式物件,用於處理Promise物件rejectresolve傳遞過來的值。

<script>

        "use strict";

        new Promise(function (resolve, reject) {
                reject("未解決");
        })
                .then(success => {
                        console.log("resolve:", success);  
                },
                        error => {
                                console.log("reject:", error);  // resolve: 未解決
                        }
                );

</script>

image-20200814144134696

catch

  每個then都可以指定第二個函式用於處理上一個Promise失敗的情況,如果每個then都進行這樣設定會顯得很麻煩,所以我們只需要使用catch即可。

  catch 可以捕獲之前所有 promise 的錯誤,所以建議將 catch 放在最後。

  建議使用 catch 處理錯誤

  將 catch 放在最後面用於統一處理前面發生的錯誤

  錯誤是冒泡操作的,下面沒有任何一個then 定義第二個函式,將一直冒泡到 catch 處理錯誤

<script>

        "use strict";

        new Promise((resolve, reject) => {
                reject("失敗");
        }).then(success => {
                console.log("成功");
        }).then(success => {
                console.log("成功");
        }).catch(error => {
                console.log(error);  // 失敗
        })

</script>

  catch也可捕捉到throw自動觸發的異常。

<script>

        "use strict";

        new Promise((resolve, reject) => {
                resolve("成功");
        }).then(success => {
                throw new Error("失敗");
        }).catch(error=>{
                console.log(error);  // Error: 失敗
        })

</script>

finally

  無論狀態是resolvereject 都會執行此動作,finally 與狀態無關。

<script>

        "use strict";

        new Promise((resolve, reject) => {
                reject("失敗");
        }).then(success => {
                console.log("成功");
        }).catch(error => {
                console.log(error);  // 失敗
        }).finally(() => {
                console.log("都會執行"); // 都會執行
        })

</script>

鏈式呼叫

  使用Promise進行鏈式呼叫,可以規避掉巢狀問題。

基本概念

  其實每一個then都是一個新的Promise,預設返回為fulfilled狀態。

<script>

        "use strict";
        
        let p1 = new Promise(function (resolve, reject) {
                resolve("已解決");
        })
        let p2 = p1.then(success => {
                console.log(success);
        }, error => {
                console.log(error);
        });

        setTimeout(() => {
                console.log(p2);  // 巨集任務佇列中的任務最後執行  Promise {<fulfilled>: undefined}
        },3000)

</script>

  此時就會產生一種鏈式關係,每一個then都是一個新的Promise物件,而每個then的作用又都是處理上個Promise物件的狀態。

  要想使用鏈式呼叫,一定要搞明白每一個then的返回值。

  返回了一個值,那麼 then 返回的 Promise將會成為接受狀態,並且將返回的值作為接受狀態的回撥函式的引數值。

  沒有返回任何值,那麼 then 返回的 Promise將會成為接受狀態,並且該接受狀態的回撥函式的引數值為 undefined

  丟擲一個錯誤,那麼 then 返回的 Promise將會成為拒絕狀態,並且將丟擲的錯誤作為拒絕狀態的回撥函式的引數值。

  返回一個已經是接受狀態的 Promise,那麼 then 返回的 Promise也會成為接受狀態,並且將那個 Promise的接受狀態的回撥函式的引數值作為該被返回的Promise的接受狀態回撥函式的引數值。

  返回一個已經是拒絕狀態的 Promise,那麼 then 返回的 Promise也會成為拒絕狀態,並且將那個 Promise的拒絕狀態的回撥函式的引數值作為該被返回的Promise的拒絕狀態回撥函式的引數值。

  返回一個未定狀態(pending)的 Promise,那麼 then 返回 Promise 的狀態也是未定的,並且它的終態與那個 Promise 的終態相同;同時,它變為終態時呼叫的回撥函式引數與那個 Promise 變為終態時的回撥函式的引數是相同的。

無返回

  上一個then無返回值時該then建立的Promise物件為resolve狀態。

  下一個then會立即執行,接收值為undefined

<script>

        "use strict";

        new Promise((resolve, reject) => {
                resolve("成功");
        }).then(success => {
                console.log("無返回1"); // 上一個Promise狀態是resolve 立刻執行
        }).then(success => {
                console.log("無返回2"); // 上一個Promise狀態是resolve 立刻執行
        })

</script>

返回值

  上一個then有返回值時該then建立的Promise物件為resolve狀態。

  下一個then會立即執行,接收值為上一個then的返回值。

<script>

        "use strict";

        new Promise((resolve, reject) => {
                resolve("成功");
        }).then(success => {
                return "v1"  // 上一個Promise狀態是resolve 立刻執行
        }).then(success => {
                console.log(success);  // v1 上一個Promise狀態是resolve 立刻執行
        })

</script>

返回Promise

  上一個then有返回值且該返回值是一個Promise物件的話下一個then會等待該Promise物件狀態改變後再進行執行,接收值根據被返回的Promise物件的任務處理狀態來決定。

<script>

        "use strict";

        new Promise((resolve, reject) => {
                resolve("成功");
        }).then(success => {
                return new Promise((resolve, reject) => {
                        // resolve("成功");  
                })
        }).then(success => {
                console.log(success);  //  上一個Promise狀態是pending 不執行,等待狀態變化
        })

</script>

巢狀解決

  我們可以利用在一個then中返回Promise下面的then會等待狀態的特性,對定時器回撥函式巢狀進行優化。

<!DOCTYPE html>
<html lang="en">

<head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
                div {
                        width: 100px;
                        height: 100px;
                        background-color: red;
                        transition: 1s;
                }

                button {

                        margin-top: 20px;
                }
        </style>
</head>

<body>
        <div></div>
        <button>點我</button>
</body>
<script>

        "use strict";

        document.querySelector("button").addEventListener("click", () => {
                new Promise(function (resolve, reject) {

                        let div = document.querySelector("div");
                        div.style.backgroundColor = "blue";
                        resolve(div);

                }).then(div => {

                        return new Promise(function (resolve, reject) {

                                setTimeout(() => {
                                        div.style.width = "50px";
                                        resolve(div);
                                }, 1000);
                        })
               
                }).then(div => {
                 

                        return new Promise(function (resolve, reject) {
                                setTimeout(() => {
                                        div.style.transform = "translate(100px)";
                                        resolve(div);
                                }, 1000);
                        })


                }).then(div => {
                        return new Promise(function (resolve, reject) {
                                setTimeout(() => {
                                        div.style.width = "100px";
                                        div.style.backgroundColor = "red";
                                        resolve(div);
                                }, 1000);
                        })

                }).then(div => {
                        return new Promise(function (resolve, reject) {
                                setTimeout(() => {
                                        div.style.backgroundColor = "yellow";
                                        resolve(div);
                                }, 1000);
                        })
                })
        });

</script>

程式碼優化

  繼續對上面的程式碼做優化。

<!DOCTYPE html>
<html lang="en">

<head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <style>
                div {
                        width: 100px;
                        height: 100px;
                        background-color: red;
                        transition: 1s;
                }

                button {

                        margin-top: 20px;
                }
        </style>
</head>

<body>
        <div></div>
        <button>點我</button>
</body>
<script>

        "use strict";

        function chain(callback, time=1000) {

                return new Promise(function (resolve, reject) {
                        setTimeout(() => {
                                let res = callback();
                                resolve(res);
                        }, time);
                });
        }

        document.querySelector("button").addEventListener("click", () => {
                new Promise(function (resolve, reject) {

                        let div = document.querySelector("div");
                        div.style.backgroundColor = "blue";
                        resolve(div);

                }).then(div => {

                        return chain(() => {
                                div.style.width = "50px";
                                return div;
                        });

                }).then(div => {


                        return chain(() => {
                                div.style.transform = "translate(100px)";
                                return div;
                        });


                }).then(div => {

                        return chain(() => {
                                div.style.width = "100px";
                                div.style.backgroundColor = "red";
                                return div;
                        })


                }).then(div => {

                        return chain(() => {
                                div.style.width = "100px";
                                div.style.backgroundColor = "yellow";
                                return div;
                        })


                })
        });

</script>

擴充套件介面

resolve

  使用 Promise.resolve() 方法可以快速的返回一個狀態是resolvePromise物件。

<script>

        "use strict";

        Promise.resolve("成功").then(success=>console.log(success)); // 成功
        
</script>

reject

  使用 Promise.reject() 方法可以快速的返回一個狀態是rejectPromise物件。

<script>

        "use strict";

        Promise.reject("失敗").then(null,error=>console.log(error)); // 失敗
        // 使用null來對成功的處理進行佔位

</script>

all

  使用Promise.all() 方法可以同時執行多個並行非同步操作,比如頁面載入時同進獲取課程列表與推薦課程。

  任何一個 Promise 執行失敗就會呼叫 catch方法

  適用於一次傳送多個非同步操作

  引數必須是可迭代型別,如Array/Set

  成功後返回 Promise 結果的有序陣列

  以下示例將展示同時提交兩個非同步操作,只有當全部成功時才會執行Promise.all()其下的then

<script>

        "use strict";

        const p1 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功");

                }, 3000);
        });

        const p2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功");

                }, 3000);
        });

        Promise.all([p1, p2])
                .then(success => {
                        console.log(success);  // (2) ["成功", "成功"]
                })
                .catch(error => {
                        console.log(error);  // 任何一個失敗都會執行這裡
                });


</script>

allSettled

  allSettled 用於處理多個Promise ,只關注執行完成,不關注是否全部執行成功,allSettled 狀態只會是fulfilled

<script>

        "use strict";

        const p1 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功");

                }, 1000);
        });

        const p2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功");

                }, 3000);
        });

        Promise.allSettled([p1, p2])
                .then(success => {
                        console.log(success);
                })
        /*
        
        [{status: "fulfilled", value: "成功"}, {status: "fulfilled", value: "成功"}]
        
        */


</script>

race

  使用Promise.race() 處理容錯非同步,和race單詞一樣哪個Promise快用哪個,哪個先返回用哪個。

  其實這在某些資源引用上比較常用,可以新增多個資源地址進行請求,誰先快就用誰的。

  以最快返回的Promise為準

  如果最快返加的狀態為rejected 那整個Promiserejected執行cache

  如果引數不是Promise,內部將自動轉為Promise

  下面示例中成功1比較快,就用成功1的。

<script>

        "use strict";

        const p1 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功1");

                }, 1000);
        });

        const p2 = new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve("成功2");

                }, 3000);
        });

        Promise.race([p1, p2])
                .then(success => {
                        console.log(success);  // 成功1
                })
 
</script>

async/await

  使用 async/awaitPromise的語法糖,可以讓編寫 Promise更清晰易懂,也是推薦編寫Promise的方式。

  async/await 本質還是Promise,只是更簡潔的語法糖書寫

async

  在某一個函式前加上async,該函式會返回一個Promise物件。

  我們可以依照標準Promise來操縱該物件。

<script>

        "use strict";

        async function get() {
                return "請求成功...";
        }

        get().then(success => {
                console.log(success);  // 請求成功...
        })

</script>

await

  使用 await 關鍵詞後會等待Promise完。

  await 後面一般是Promise,如果不是直接返回

  await 必須放在 async定義的函式中使用

  await 用於替代 then 使編碼更優雅

<script>

        "use strict";

        async function get() {
                const ajax = new Promise((resolve, reject) => {
                        setTimeout(()=>{
                                resolve("返回的結果");
                        },3000);
                });

                let result = await ajax;
                console.log(result);  // 返回的結果
        }

        get();

</script>

  一般await後面是外部其它的Promise物件

<script>

        "use strict";

        async function getName() {
                return new Promise((resolve, reject) => {
                        resolve("姓名資料...");
                });
        }

        async function getGrades() {
                return new Promise((resolve, reject) => {
                        resolve("成績資料...");
                });
        }

        async function run() {

                let nameSet = await getName();
                let gradesSet = await getGrades();

                console.log(nameSet);
                console.log(gradesSet);

        }

        run();

</script>

異常處理

  Promise狀態為rejected其實我們就可以將它歸為出現異常了。

  當一個await發生異常時,其他的await不會進行執行。

<script>

        "use strict";

        async function getName() {
                return new Promise((resolve, reject) => {
                        reject("姓名資料獲取失敗...");
                })

        }


        async function getGrades() {
                return new Promise((resolve, reject) => {
                        resolve("成績資料...");
                });
        }

        async function run() {
                let nameSet = await getName();  // Uncaught (in promise) 姓名資料獲取失敗...
                let gradesSet = await getGrades(); // 不執行
        }

        run();

</script>

  如果在async中不確定會不會丟擲異常,我們可以在接收時使用catch進行處理。

<script>

        "use strict";

        async function getName() {
                return new Promise((resolve, reject) => {
                        reject("姓名資料獲取失敗...");
                })
                
        }

        async function run() {
                let nameSet = await getName().catch(error => console.log(error));
        }

        run();

</script>

  更推薦寫成下面這種形式

<script>

        "use strict";

        async function getName() {
                return new Promise((resolve, reject) => {
                        reject("姓名資料獲取失敗...");
                })
                .catch(error => console.log(error));
        }

        async function run() {
                let nameSet = await getName();

        }

        run();

</script>

  也可使用try...catch進行處理。

<script>

        "use strict";

        async function getName() {
                return new Promise((resolve, reject) => {
                        reject("姓名資料獲取失敗...");
                });
        }

        async function run() {
                try {
                        let nameSet = await getName();
                } catch (e) {
                        console.log(e);  // 姓名資料獲取失敗...
                }
        }

        run();

</script>

相關文章