1. 程式人生 > 其它 >掌握JavaScript中的Promise,實現非同步程式設計

掌握JavaScript中的Promise,實現非同步程式設計

事件迴圈

基本介紹

JavaScript是一門單執行緒的程式語言,所以沒有真正意義上的並行特性。

為了協調事件處理、頁面互動、指令碼呼叫、UI渲染、網路請求等行為對主執行緒造成的影響,事件迴圈(event loop)方案應運而生。

事件迴圈說白了就是一個不斷的在等待任務、執行任務的方案。

在JavaScript中,根據執行方式的不同,有2種狀態的任務,分別是同步任務和非同步任務。

同步任務率先執行,而後執行非同步任務,所有的非同步任務由2個佇列儲存,分別是:

  • 微任務佇列
  • 巨集任務佇列

主執行緒在執行完同步任務後,會不斷的從這2個任務佇列中按照先進先出的策略取出非同步任務並執行。

並且在此期間也會有新的事件不斷的加入至各個任務佇列中,以此迴圈往復、永不阻塞。

如下圖所示:

任務分類

巨集任務包括:

  • setInterval
  • setTimeout
  • setTimmediate Node.Js獨有
  • XHR callbackfn
  • event callbackfn
  • requestAnimationFrame
  • UI rendering

微任務包括:

  • Promise.then
  • catch finally
  • process.nextTick Node.Js獨有
  • MutationObserver

執行順序

根據任務的狀態,任務的執行優先順序也會有所不同,具體執行順序如下所示:

  1. 同步任務(sync-task)
  2. 微任務(micro-task)
  3. 巨集任務(macro-task)

而關於微任務和巨集任務的執行,還有更詳細的劃分:

  • 微任務佇列中一旦有任務,將全部執行完成後再執行巨集任務
  • 巨集任務佇列中的任務在執行完成後,會檢查微任務佇列中是否有新新增的任務,如果有,那麼將執行微任務佇列中所有新新增的任務,如果沒有則繼續執行下一個巨集任務

如下圖所示:

程式碼測試:

"use strict";

// 巨集任務,每5s新增一個微任務並執行
setInterval(() => {
    async function foo() {
        return "micro-task"
    }

    async function bar() {
        let result = await foo();
        console.log(result);
    }

    bar();

}, 5000);

// 巨集任務,每1s執行一次
setInterval(() => { console.log("macro-task"); }, 1000);

// 同步任務
(() => {
    console.log("hello world");
})();

測試結果,雖然同步任務的程式碼在最下面,但是它會最先執行,而每新增一個微任務時,巨集任務的執行會被插隊:

Promise

認識Promise

Promise是ES6中出現的新功能,用於在JavaScript中更加簡單的實現非同步程式設計。

我們可以使用new Promise()創建出一個Promise物件,它接收一個執行器函式,該函式需要指定resolve和reject引數用於改變當前Promise物件的執行狀態。

由於Promise物件中執行器程式碼是屬於同步任務,所以他會率先的進行執行,一個Promise物件擁有以下幾種狀態:

  • fulfilled:任務完成、使用resolve改變了任務狀態
  • rejected:任務失敗、使用reject改變了任務狀態,或任務執行中丟擲了異常
  • pending:正在等待、未使用resolve或reject改變任務狀態

注意,每個Promise物件的狀態只允許改變一次!不可以多次更改。

示例如下。

1)Promise中執行器任務是同步任務,所以會率先執行:

"use strict";

setInterval(() => { console.log("macro task 3"); }, 1000)

let task = new Promise((resolve, reject) => {
    console.log("sync task 1");
});

console.log("sync task 2");

// sync task 1
// sync task 2
// macro task 3

2)使用resolve改變Promise物件的狀態為fulfilled:

"use strict";

let task = new Promise((resolve, reject) => {
    let x = Math.floor(Math.random() * 100) + 1;
    let y = Math.floor(Math.random() * 100) + 1;
    let result = x + y;

    // 返回結果為resolve()中的值
    resolve(result);
});


console.log(task);

// Promise{<fulfilled>: 83}

3)使用reject改變Promise物件的狀態為rejected, 它將引發一個異常:

"use strict";

let task = new Promise((resolve, reject) => {
    let x = Math.floor(Math.random() * 100) + 1;
    let y = Math.floor(Math.random() * 100) + 1;
    let result = x + y;

    // 返回結果為reject()中的值
    reject("error!")
});


console.log(task);

// Promise{<rejected>: "error!"}
// Uncaught (in promise) error!

4)如果未使用resolve或reject改變Promise物件狀態,那麼該任務的狀態將為pending:

"use strict";

let task = new Promise((resolve, reject) => {
    let x = Math.floor(Math.random() * 100) + 1;
    let y = Math.floor(Math.random() * 100) + 1;
    let result = x + y;
});


console.log(task);

// Promise{<pending>}

then()

我們可以在Promise物件後,新增一個用於處理任務狀態的回撥then()方法。

then()方法只有在Promise物件狀態為fulfilled或者rejected時才會進行執行,它具有2個引數,接收2個回撥函式:

  • onfulfilled:Promise物件狀態為fulfilled將執行該函式,具有1個引數value,接收Promise任務中resolve()所傳遞的值
  • onrejected:Promise物件狀態為rejected將執行該函式,具有1個引數reason,接收Promise任務中reject()或異常發生時所傳遞的值

此外,then()方法是屬於微任務,所以他會插在巨集任務之前進行執行。

程式碼示例如下:

1)Promise物件狀態為fulfilled,執行then()方法的第1個回撥函式:

"use strict";

let task = new Promise((resolve, reject) => {
    resolve("success");
}).then(
    value => {
        console.log(value);
    },
    reason => {
        console.log(reason);
    });


// success

2)Promise物件狀態為rejected,執行then()方法的第2個回撥函式:

"use strict";

let task = new Promise((resolve, reject) => {
    throw new Error("error");
}).then(
    value => {
        console.log(value);
    },
    reason => {
        console.log(reason);
    });


// error

then()鏈式呼叫

其實每一個then()都將返回一個全新的Promise,預設情況下,該Promise的狀態是fulfilled。

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

這意味著我們可以無限的鏈式排列then(),如下所示:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);
            return value
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);
            return value
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then3 fulfilled", value);
            return value
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then2 fulfilled success
// then3 fulfilled success

then()的返回值

要想真正的瞭解鏈式呼叫,就必須搞明白每個then()在不同狀態下的返回值對下一個then()的影響。

具體情況如下所示:

1)當前then()無返回值,則當前Promise狀態則為fulfilled。

下一個then()的onfulfilled回撥函式引數value為undefined:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);  // success
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);  // undefined
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then2 fulfilled undefined

2)當前then()有返回值,則當前Promise狀態則為fulfilled。

下一個then()的onfulfilled回撥函式引數value為當前then()的返回值:

程式碼示例:

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);  // success
            return "then1 value"
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);  // then1 value
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then2 fulfilled then1 value

3)當前then()有返回值,且返回了一個狀態為fulfilled的Promise物件。

下一個then()的onfulfilled回撥函式引數value為當前then()中被返回Promise裡resolve()所傳遞的值:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);  // success
            return new Promise((resolve, reject) => {
                resolve("then1 Promise success")
            })
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);  // then1 Promise success
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then2 fulfilled then1 Promise success

4)當前then()有返回值,且返回了一個狀態為rejected的Promise物件。

下一個then()的onrejected回撥函式引數reason為當前then()中被返回Promise裡reject()所傳遞的值,或者是被返回Promise裡丟擲異常的值:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);  // success
            return new Promise((resolve, reject) => {
                reject("then1 Promise error")
            })
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);
        },
        reason => {
            console.log(reason);  // then1 Promise error
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then1 Promise error

5)當前then()有返回值,且返回了一個狀態為pending的Promise物件。下一個then()則必須等待當前then()中被返回Promise物件狀態發生改變後才能繼續執行:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);  // success
            return new Promise((resolve, reject) => {
                console.log("pending");
            })
        },
        reason => {
            console.log(reason);
        })
    .then(
        value => {
            console.log("then2 fulfilled", value);
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// pending

另外,如果在程式碼執行時丟擲了異常,那麼返回的Promise物件狀態則為rejected,下一個then()的onrejected回撥函式引數reason為當前then()中丟擲異常的值,這裡不再進行演示。

then()穿透

then()是具有穿透功能的,當一個then()沒有指定需要被執行的回撥函式時,它將繼續冒泡向下傳遞:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then()
    .then(
        value => {
            console.log("then2 fulfilled", value);
        },
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then2 fulfilled success

catch()

每個then()都可以指定onrejected回撥函式用於處理上一個Promise狀態為rejected的情況。如果每個then()都進行這樣的設定會顯得很麻煩,所以我們只需要使用catch()即可。

catch()可以捕獲之前所有Promise的錯誤執行,故建議將catch()放在最後。

catch()需要指定一個回撥函式onrejected,具有1個引數reason,接收Promise任務中reject()或異常發生時所傳遞的值。

錯誤是冒泡傳遞的,如果沒有任何一個then()定義onrejected的回撥函式,那麼錯誤將一直冒泡到catch()處進行處理:

程式碼示例:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);
            return value
        }
    )
    .then(
        value => {
            console.log("then2 rejected", value);
            throw new Error("error");
        })
    .then(
        value => {
            console.log("then3 ...", value);
        }
    )
    .catch(
        reason => {
            console.log(reason);
        });

// first Promise task status is fulfilled
// then1 fulfilled success
// then2 rejected success
// Error: error

finally()

finally()是無論任務處理成功或者失敗都會執行,因此建議將它放在鏈式呼叫的最後面。

它需要指定一個回撥函式onfinally,該回調函式沒有任何引數:

"use strict";

let task = new Promise((resolve, reject) => {
    console.log("first Promise task status is fulfilled");
    resolve("success");
})
    .then(
        value => {
            console.log("then1 fulfilled", value);
            return value
        }
    )
    .catch(
        reason => {
            console.log(reason);
        }
    )
    .finally(
        () => {
            console.log("run");
        }
    );

// first Promise task status is fulfilled
// then1 fulfilled success
// run

擴充套件方法

resolve()

resolve()方法用於快速返回一個狀態為fulfilled的Promise物件,生產環境中使用較少:

"use strict";

let task = Promise.resolve("success");

console.log(task);

// Promise{<fulfilled>: "success"}

reject()

reject()方法用於快速返回一個狀態為rejected的Promise物件,生產環境中使用較少:

"use strict";

let task = Promise.reject("error");

console.log(task);

// Promise{<rejected>: "error"}
// Uncaught (in promise) error

all()

all()方法用於一次同時執行多個非同步任務,並且必須確保這些任務是成功的。

  • all()方法接收的引數必須是可迭代型別,如Array、map、set
  • 任何一個Promise狀態為rejected,都將呼叫catch()方法
  • 當所有任務成功執行後,將返回一個包含所有任務的執行結果陣列

all()方法應用場景還是非常廣泛的,如我們需要使用Ajax請求後端的書籍與價格資訊時,不論是書籍獲取失敗還是價格獲取失敗,都將認為此次任務的失敗。

示例如下:

"use strict";

const getBookNameTask = new Promise((resolve, reject) => {
    // 模擬請求後端的書籍名稱,需要花費3s
    setTimeout(() => {
        resolve(JSON.stringify(
            ["HTML", "CSS", "JavaScript"]
        ))
    }, 3000);
});

const getBookPriceTask = new Promise((resolve, reject) => {
    // 模擬請求後端的書籍價格,需要花費5s
    setTimeout(() => {
        resolve(JSON.stringify(
            [98, 120, 40]
        ))
    }, 5000);
})

// 執行任務
Promise.all(
    [getBookNameTask, getBookPriceTask]
)
    .then(value => {
        // 書籍和價格全部獲取後才執行這裡
        // value = ["[\"HTML\",\"CSS\",\"JavaScript\"]", "[98,120,40]"]
        const bookNameArray = JSON.parse(value[0]);
        const bookPriceArray = JSON.parse(value[1]);
        const bookAndNameMap = new Map();
        for (let i = 0; i < bookNameArray.length; i++) {
            bookAndNameMap.set(bookNameArray[i], bookPriceArray[i]);
        }
        console.log(bookAndNameMap);
    })
    .catch(reason => {
        // 任何一個沒獲取到都執行這裡
        console.log(reason);
    });

allSettled()

allSettled()方法和all()方法相似,都是用於同時執行多個非同步任務,但是它並不關心所有任務是否都執行成功。

allSettled()的狀態只會是fulfilled:

"use strict";

const getBookNameTask = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("error! Can't query all books name")
    }, 3000);
});

const getBookPriceTask = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("error! Can't query all books price")
    }, 5000);
})

// 執行任務
Promise.allSettled(
    [getBookNameTask, getBookPriceTask]
)
    .then(value => {
        // 不管怎樣都會執行這裡
        console.log("run me");
    })

race()

race()也可同時執行多個任務,它僅會返回最快完成任務的執行結果。

  • 以最快返回的任務結果為準
  • 如果最快返回的任務狀態為rejected,那麼race()的狀態也將視為rejected,此時將執行catch()方法

race()方法用的也比較多,如我們需要載入一些圖片,這些圖片在多個服務端上都有儲存,但為了提高使用者體驗我們需要根據使用者所在的地理位置選擇最近的伺服器,此時race()就派上了用場:

"use strict";

const getCacheImages = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("get cache images success!!");
    }, 1000);
})

const getWebImages = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("get web images success!!");
    }, 3000);
})

// 建立任務
Promise.race(
    [getCacheImages, getWebImages]
)
    .then(value => {
        console.log(value);
    })
    .catch(reason => {
        console.log(reason);
    })

// get cache images success!!

async&await

async

async其實是new Promise()的語法糖簡寫形式。

在某一個函式前面加上async,執行該函式時將會返回一個Promise物件。

  • 沒有return:返回的Promise物件狀態為fulfilled,下一個then()的onfulfilled回撥函式引數value為undefined
  • 直接return:返回的Promise物件狀態為fulfilled,下一個then()的onfulfilled回撥函式引數value為當前async函式的返回值
  • return了一個狀態為fulfilled的Promise物件:下一個then()的onfulfilled回撥函式引數value為當前async函式中被返回Promise裡resolve()所傳遞的值
  • return了一個狀態為rejected的Promise物件:下一個then()的onrejected回撥函式引數reason為當前async函式中被返回Promise裡reject()所傳遞的值,或者是被返回Promise裡丟擲異常的值
  • 執行時丟擲異常:返回的Promise物件狀態為rejected,下一個then()的onrejected回撥函式引數reason為當前async函式中丟擲異常的值

示例演示:

"use strict";

async function task() {
    return "success"
}

task().then(value => {
    console.log(value);
});

// success

await

await其實是then()的另一種寫法,它只能在async函式中使用。

  • await後面一般都會跟上一個Promise物件,如果不是Promise物件,將直接返回該值。
  • await使用必須在async函式中
  • await作為then()的語法糖形式,使用它編寫程式碼將使程式碼變的更加優雅

如下所示,我們有3個任務,這3個任務必須是先通過使用者ID獲取人員姓名、再通過使用者ID獲取資訊ID、最後再通過使用者ID獲取人員資訊。

如果你用純Promise+then()的方式進行程式碼編寫,它將是這樣的:

"use strict";

const idAndName = new Map([
    [1, "Jack"],
    [2, "Tom"],
    [3, "Mary"],
]);

const personnelInformation = new Map([
    [1, { gender: "female", age: 18, addr: "TianJin", desc: "my name is Mary" }],
    [2, { gender: "male", age: 21, addr: "ShangHai", desc: "my name is Tom" }],
    [3, { gender: "male", age: 18, addr: "BeiJing", desc: "my name is Jack" }],
]);

const nameAndMessage = new Map([
    [1, 3],
    [2, 2],
    [3, 1],
])



function getUserMessage(id) {
    let userName, messageId, message, str;

    new Promise((resolve, reject) => {
        // 獲取姓名
        if (idAndName.has(id)) {
            userName = idAndName.get(id);
            resolve();
        }
        reject(`no information id : ${id}`);
    })
        .then(() => {
            // 獲取關係
            messageId = nameAndMessage.get(id);
        })
        .then(() => {
            // 獲取資訊
            message = personnelInformation.get(messageId);
        })
        .then(() => {
            // 進行渲染
            str = `name : ${userName}</br>`;
            for (let [k, v] of Object.entries(message)) {
                str += `${k} : ${v}</br>`;
            }
            document.write(str)
        })
        .catch(reason => {
            document.write(`<p style="color:red">${reason}</p>`);
        })
}

getUserMessage(3);

如果你使用async+awit的方式編寫,那麼它的邏輯就會清楚很多:

"use strict";

const idAndName = new Map([
    [1, "Jack"],
    [2, "Tom"],
    [3, "Mary"],
]);

const personnelInformation = new Map([
    [1, { gender: "female", age: 18, addr: "TianJin", desc: "my name is Mary" }],
    [2, { gender: "male", age: 21, addr: "ShangHai", desc: "my name is Tom" }],
    [3, { gender: "male", age: 18, addr: "BeiJing", desc: "my name is Jack" }],
]);

const nameAndMessage = new Map([
    [1, 3],
    [2, 2],
    [3, 1],
])

// 獲取姓名
async function getName(id) {
    if (idAndName.has(id)) {
        return idAndName.get(id);
    }
    throw new Error(`no information id : ${id}`);
}

// 獲取關係
async function getRelation(id) {
    return nameAndMessage.get(id);
}

// 獲取資訊
async function getMessage(messageId) {
    return personnelInformation.get(messageId);
}

// 入口函式,進行渲染
async function getUserMessage(id) {
    try {
        let userName = await getName(id);  // 必須等待該函式執行完成才會繼續向下執行
        let messageId = await getRelation(id);
        let message = await getMessage(messageId);
        let str = `name : ${userName}</br>`;

        for (let [k, v] of Object.entries(message)) {
            str += `${k} : ${v}</br>`;
        }

        document.write(str)
    } catch (e) {
        document.write(`<p style="color:red">${e}</p>`);
    }

}


getUserMessage(3);

異常處理

async+await的異常處理推薦使用try+catch語句將所有執行程式碼進行包裹,它將處理所有可能出現的異常,相當於在鏈式呼叫的最後面加上catch()方法:

"use strict";

async function task01() {
    console.log("run task 01");
}

async function task02() {
    throw new Error("task02 error");
    console.log("run task 02");
}

async function task03() {
    console.log("run task 03");
}

async function main() {
    try {
        await task01();
        await task02();
        await task03();
    } catch (e) {
        console.log(e);
    }
}

main();

也可以在主函式外部使用catch()方法來處理異常,但是我並不推薦這麼做。

"use strict";

async function task01() {
    console.log("run task 01");
}

async function task02() {
    throw new Error("task02 error");
    console.log("run task 02");
}

async function task03() {
    console.log("run task 03");
}

async function main() {
    await task01();
    await task02();
    await task03();
}

main().catch(reason => {
    console.log(reason);
});

除此之外,你也可以使用try+catch語句塊對單獨的async函式語句塊進行處理,預防可能出現的異常。