當前位置: 妍妍網 > 碼農

8 個關於 Promise 高級用途的技巧

2024-03-06碼農

前言

大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心~

我發現很多人只知道如何常規地使用promise。

在js計畫中,promise的使用應該是必不可少的,但我發現在同事和面試官中,很多中級以上的前端仍然堅持promiseInst.then()、promiseInst.catch()、Promise等常規用法等等。即使是 async/await 他們也只知道它但不知道為什麽要使用它。

但實際上,Promise 有很多巧妙的高級用法,並且一些高級用法在 alova 請求策略庫內部也被廣泛使用。

現在,我將與大家分享8個高級使用技巧。希望這些技巧能夠對你有所幫助,現在,我們就開始吧。

1. Promise陣列的序列執行

例如,如果你有一組介面需要序列執行,你可能首先想到使用await。

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];for (const requestItem of requestAry) {await requestItem();}

如果使用promise,可以使用then函式串聯多個promise,實作序列執行。

const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];const finallyPromise = requestAry.reduce((currentPromise, nextRequest) => currentPromise.then(() => nextRequest()),Promise.resolve() // Create an initial promise for linking promises in the array);

2. 在新的 Promise 範圍之外更改狀態

假設你有多個頁面,其功能要求在允許使用之前收集使用者資訊。點選使用某個功能之前,會彈出一個彈框進行資訊收集。你會如何實施這個?

以下是不同級別前端同學的實作思路:

初級前端:我寫一個模態框,然後復制貼上到其他頁面。效率非常高!

中級前端:這個不好維護。我們需要單獨封裝這個元件,並在需要的頁面引入!

高級前端:安裝任何密封的東西!!!把方法呼叫寫在所有頁面都可以呼叫的地方不是更好嗎?

想要了解高級前端是如何實作的,以vue3為例,看一下下面的例子。

<!-- App.vue --><template><!-- The following is the modal box component --><div class="modal"v-show="visible"><div> User name: <inputv-model="info.name" /></div><!-- Other information --><button @click="handleCancel">Cancel</button><button @click="handleConfirm">Submit</button></div><!-- Page components --></template><scriptsetup>import { provide } from'vue';const visible = ref(false);const info = reactive({name: ''});let resolveFn, rejectFn;// Pass the information collection function to the followingprovide('getInfoByModal', () => { visible.value = true;returnnewPromise((resolve, reject) => {// Assign the two functions to the outside and break through the promise scope resolveFn = resolve; rejectFn = reject; });})const handleConfirm = () => { resolveFn && resolveFn(info);};const handleCancel = () => { rejectFn && rejectFn(newError('User has canceled'));};</script>

接下來,getInfoByModal就可以透過直接呼叫模態框來輕松獲取使用者填寫的數據。

<template><button @click="handleClick">Fill in the information</button></template><scriptsetup>import { inject } from'vue';const getInfoByModal = inject('getInfoByModal');const handleClick = async () => {// After the call, the modal box will be displayed. After the user clicks to confirm, the promise will be changed to the fullfilled state to obtain the user information.const info = await getInfoByModal();await api.submitInfo(info);}</script>

這也是很多UI元件庫中封裝常用元件的一種方式。

3. async/await 的替代用法

很多人只知道它是用來在呼叫await時接收async函式的返回值的,卻不知道async函式它實際上是一個返回promise的函式。例如,以下兩個函式是等效的:

const fn1 = async () => 1;const fn2 = () =>Promise.resolve(1);fn1(); // Also returns a promise object with a value of 1

在大多數情況下,await 會跟隨 Promise 物件並等待它完全填充。因此,下面的 fn1 函式 wait 也是等價的:

await fn1();const promiseInst = fn1();await promiseInst;

然而,await也有一個鮮為人知的秘密。當它後面跟的值不是promise物件時,它會用promise物件包裝該值,所以await後面的程式碼必須異步執行。例子:

Promise.resolve().then(() => {console.log(1);});await2;console.log(2);//Print order bits: 1 2

相當於

Promise.resolve().then(() => {console.log(1);});Promise.resolve().then(() => {console.log(2);});

4. 承諾實施請求共享

當一個請求已經發出但尚未得到響應時,再次發出相同的請求,就會造成請求的浪費。此時,我們可以將第一個請求的響應與第二個請求共享。

request('GET', '/test-api').then(response1 => {// ...});request('GET', '/test-api').then(response2 => {// ...});

上述兩個請求實際上只發送一次,同時收到相同的響應值。

那麽,請求共享有哪些使用場景呢?我認為有以下三個:

  • 當頁面渲染多個內部元件同時獲取數據時;

  • 送出按鈕未禁用且使用者連續多次點選送出按鈕;

  • 預載入數據的情況下,預載入完成之前進入預載入頁面;

  • 這也是alova的高級功能之一。要實作請求共享,需要使用promise的緩存功能,即一個promise物件可以透過多次await獲取數據。簡單的實作思路如下:

    const pendingPromises = {};functionrequest(type, url, data) {// Use the request information as the only request key to cache the promise object being requested//Requests with the same key will reuse promiseconst requestKey = JSON.stringify([type, url, data]);if (pendingPromises[requestKey]) {return pendingPromises[requestKey]; }const fetchPromise = fetch(url, { method: type, data: JSON.stringify(data) }) .then(response => response.json()) .finally(() => {delete pendingPromises[requestKey]; });return pendingPromises[requestKey] = fetchPromise;}

    上述兩個請求實際上只發送一次,同時收到相同的響應值。

    那麽,請求共享有哪些使用場景呢?我認為有以下三個:

  • 當頁面渲染多個內部元件同時獲取數據時;

  • 送出按鈕未禁用且使用者連續多次點選送出按鈕;

  • 預載入數據的情況下,預載入完成之前進入預載入頁面;

  • 這也是alova的高級功能之一。要實作請求共享,需要使用promise的緩存功能,即一個promise物件可以透過多次await獲取數據。簡單的實作思路如下:

    const promise = newPromise((resolve, reject) => { resolve(); reject();});

    正確答案是已滿狀態。我們只需要記住,一旦待處理的promise從一種狀態轉移到另一種狀態,就無法更改。因此,例子中是先轉為fulfilled狀態,然後reject()就不會再轉為rejected狀態。

    6.徹底明確then/catch/finally返回值

    一句話概括就是,上面三個函式都會返回一個新的promise包裝物件。包裝後的值是執行回呼函式的返回值。如果回呼函式丟擲錯誤,它將包裝拒絕狀態承諾。似乎不太容易理解,我們來看一個例子:

    我們可以將它們一一復制到瀏覽器控制台並執行它們以幫助理解。

    // then functionPromise.resolve().then(() =>1); // The return value is new Promise(resolve => resolve(1))Promise.resolve().then(() =>Promise.resolve(2)); // Return new Promise(resolve => resolve(Promise.resolve(2)))Promise.resolve().then(() => {thrownewError('abc')}); // Return new Promise(resolve => resolve(Promise.reject(new Error('abc'))))Promise.reject().then(() =>1, () =>2); // The return value is new Promise(resolve => resolve(2))//catch functionPromise.reject().catch(() =>3); // The return value is new Promise(resolve => resolve(3))Promise.resolve().catch(() =>4); // The return value is new Promise(resolve => resolve(promise object that calls catch))//When the finally function returns a non-promise value, return the promise object before the finally function.Promise.resolve().finally(() => {}); // Return Promise.resolve()Promise.reject().finally(() => {}); // Return Promise.reject()// When the return value of the finally function is promise, wait for the returned promise to be parsed before returning the promise object before the finally function.Promise.resolve(5).finally(() =>newPromise(res => { setTimeout(res, 1000);})); // Return the Promise in pending status, which will be resolved to 5 after 1 second.Promise.reject(6).finally(() =>newPromise(res => { setTimeout(res, 1000);})); // Return the Promise in the pending state, and throw the number 6 after 1 second

    7、then函式的第二次回呼和catch回呼有什麽區別?

    當請求發生錯誤時,會觸發 Promise 的 then 的第二個回呼函式和 catch。乍一看沒有區別,但實際上前者無法捕獲then當前第一個回呼函式中丟擲的錯誤,但catch可以。

    Promise.resolve().then(() => {thrownewError('Error from success callback'); },() => {// will not be executed }).catch(reason => {console.log(reason.message); // Will print out "error from success callback"});

    原理就如上一點所說的。catch 函式是在 then 函式返回的處於拒絕狀態的 Promise 上呼叫的,因此它的錯誤自然可以被捕獲。

    8.(最終)Promise實作koa2洋蔥中介軟體模型

    koa2框架引入了洋蔥模型,可以讓你的請求像剝洋蔥一樣一層層進去,再一層層出來,從而實作請求前後處理的統一。

    我們來看一個簡單的 koa2 洋蔥模型:

    const app = new Koa();app.use(async (ctx, next) => {console.log('a-start');await next();console.log('a-end');});app.use(async (ctx, next) => {console.log('b-start');await next();console.log('b-end');});app.listen(3000);

    上面的輸出是a-start -> b-start -> b-end -> a-end,這樣神奇的輸出序列是如何實作的呢?有人沒天賦,簡單的用20行左右的程式碼就實作了。如有雷同,純屬巧合。

    接下來我們分析一下

    註:以下內容對新手不友好,請謹慎閱讀。

    首先先保存中介軟體函式,在listen函式中收到請求後呼叫洋蔥模型執行。

    functionaction(koaInstance, ctx){// ...} classKoa{ middlewares = [];use(mid) {this.middlewares.push(mid); } listen(port) {// Pseudocode simulates receiving request http.on('request', ctx => { action(this, ctx); }); }}

    收到請求後,從第一個中介軟體開始序列執行next之前的前置邏輯。

    //Start to start middleware callfunctionaction(koaInstance, ctx) {let nextMiddlewareIndex = 1; // Identifies the next middleware index to be executed//Define next functionfunctionnext() {// Before peeling the onion, calling next will call the next middleware functionconst nextMiddleware = middlewares[nextMiddlewareIndex];if (nextMiddleware) { nextMiddlewareIndex++; nextMiddleware(ctx, next); } }//Start execution from the first middleware function and pass in the ctx and next functions middlewares[0](ctx, next);}

    處理next之後的post邏輯

    functionaction(koaInstance, ctx) {let nextMiddlewareIndex = 1;functionnext() {const nextMiddleware = middlewares[nextMiddlewareIndex];if (nextMiddleware) { nextMiddlewareIndex++;// A return is also added here to allow the execution of the middleware function to be executed in series from back to front using promises (it is recommended to understand this return repeatedly)returnPromise.resolve(nextMiddleware(ctx, next)); } else {// When the pre-logic of the last middleware is executed, return the fullyfilled promise and start executing the post-logic after next.returnPromise.resolve(); } } middlewares[0](ctx, next);}

    至此,一個簡單的洋蔥模型就已經實作了。

    總結

    以上就是我今天想與你分享的8個關於Promise的高級用途的全部內容,如果你覺得有用的話,請記得點贊我,關註我,並將這個內容分享給你的小夥伴們,也許能夠幫助到他。

    結語

    我是林三心

  • 一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;

  • 一個偏前端的全幹工程師;

  • 一個不正經的金塊作者;

  • 逗比的B站up主;

  • 不帥的小紅書博主;

  • 喜歡打鐵的籃球菜鳥;

  • 喜歡歷史的乏味少年;

  • 喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 -->

  • 廣州的兄弟可以約飯哦,或者約球~我負責打鐵,你負責進球,謝謝~