JavaScript Promise對象

事件循環

  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>