前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心~
在 js 計畫中,promise 的使用應該是必不可少的,但我發現在同事和面試者中,很多中級或以上的前端都還停留在promiseInst.then()
、promiseInst.catch()
、Promise.all
等常規用法,連async/await
也只是知其然,而不知其所以然。
但其實,promise 還有很多巧妙的高級用法,也將一些高級用法在 alova 請求策略庫內部大量運用。
現在,我把這些毫無保留地在這邊分享給大家,看完你應該再也不會被問倒了, 最後還有壓軸題哦 。
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(); // 建立一個初始promise,用於連結陣列內的promise
);
2. 在 new Promise 作用域外更改狀態
假設你有多個頁面的一些功能需要先收集使用者的資訊才能允許使用,在點選使用某功能前先彈出資訊收集的彈框,你會怎麽實作呢?
以下是不同水平的前端同學的實作思路:
初級前端 :我寫一個模態框,然後復制貼上到其他頁面,效率很杠杠的!
中級前端 :你這不便於維護,我們要單獨封裝一下這個元件,在需要的頁面引入使用!
高級前端 :封什麽裝什麽封!!!寫在所有頁面都能呼叫的地方,一個方法呼叫豈不更好?
看看高級前端怎麽實作的,以 vue3 為例來看看下面的範例。
<!-- App.vue -->
<template>
<!-- 以下是模態框元件 -->
<div class="modal"v-show="visible">
<div>
使用者姓名:<inputv-model="info.name" />
</div>
<!-- 其他資訊 -->
<button @click="handleCancel">取消</button>
<button @click="handleConfirm">送出</button>
</div>
<!-- 頁面元件 -->
</template>
<scriptsetup>
import { provide } from'vue';
const visible = ref(false);
const info = reactive({
name: ''
});
let resolveFn, rejectFn;
// 將資訊收集函式函式傳到下面
provide('getInfoByModal', () => {
visible.value = true;
returnnewPromise((resolve, reject) => {
// 將兩個函式賦值給外部,突破promise作用域
resolveFn = resolve;
rejectFn = reject;
});
})
const handleConfirm = info => {
resolveFn && resolveFn(info);
};
const handleCancel = () => {
rejectFn && rejectFn(newError('使用者已取消'));
};</script>
接下來直接呼叫
getInfoByModal
即可使用模態框,輕松獲取使用者填寫的數據。
<template>
<button @click="handleClick">填寫資訊</button>
</template>
<script setup>
import { inject } from 'vue';
const getInfoByModal = inject('getInfoByModal');
const handleClick = async () => {
// 呼叫後將顯示模態框,使用者點選確認後會將promise改為fullfilled狀態,從而拿到使用者資訊
const info = await getInfoByModal();
await api.submitInfo(info);
}
</script>
這也是很多 UI 元件庫中對常用元件的一種封裝方式。
3. async/await 的另類用法
很多人只知道在
async函式
呼叫時用
await
接收返回值,但不知道
async函式
其實就是一個返回 promise 的函式,例如下面兩個函式是等價的:
const fn1 = async () => 1;
const fn2 = () =>Promise.resolve(1);
fn1(); // 也返回一個值為1的promise物件
而
await
在大部份情況下在後面接 promise 物件,並等待它成為 fullfilled 狀態,因此下面的 fn1 函式等待也是等價的:
await fn1();
const promiseInst = fn1();
await promiseInst;
然而,await 還有一個鮮為人知的秘密 ,當後面跟的是非 promise 物件的值時,它會將這個值使用 promise 物件包裝,因此 await 後的程式碼一定是異步執行的。如下範例:
Promise.resolve().then(() => {
console.log(1);
});
await2;
console.log(2);
// 打印順序位:1 2
等價於
Promise.resolve().then(() => {
console.log(1);
});
Promise.resolve().then(() => {
console.log(2);
});
4. promise 實作請求共享
當一個請求已發出但還未響應時,又發起了相同請求,就會造成了請求浪費,此時我們就可以將第一個請求的響應共享給第二個請求。
request('GET', '/test-api').then(response1 => {
// ...
});
request('GET', '/test-api').then(response2 => {
// ...
});
上面兩個請求其實只會真正發出一次,並且同時收到相同的響應值。
那麽,請求共享會有哪幾個使用場景呢?我認為有以下三個:
當一個頁面同時渲染多個內部自獲取數據的元件時;
送出按鈕未被禁用,使用者連續點選了多次送出按鈕;
在預載入數據的情況下,還未完成預載入就進入了預載入頁面;
這也是 alova [1] 的高級功能之一,實作請求共享需要用到 promise 的緩存功能,即一個 promise 物件可以透過多次 await 獲取到數據,簡單的實作思路如下:
const pendingPromises = {};
functionrequest(type, url, data) {
// 使用請求資訊作為唯一的請求key,緩存正在請求的promise物件
// 相同key的請求將復用promise
const 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;
}
5. 同時呼叫 resolve 和 reject 會怎麽樣?
大家都知道 promise 分別有
pending/fullfilled/rejected
三種狀態,但例如下面的範例中,promise 最終是什麽狀態?
const promise = newPromise((resolve, reject) => {
resolve();
reject();
});
正確答案是
fullfilled
狀態,我們只需要記住,promise 一旦從
pending
狀態轉到另一種狀態,就不可再更改了,因此範例中先被轉到了
fullfilled
狀態,再呼叫
reject()
也就不會再更改為
rejected
狀態了。
6. 徹底理清 then/catch/finally 返回值
先總結成一句話,就是 以上三個函式都會返回一個新的 promise 包裝物件,被包裝的值為被執行的回呼函式的返回值,回呼函式丟擲錯誤則會包裝一個 rejected 狀態的 promise。 ,好像不是很好理解,我們來看看例子:
// then函式
Promise.resolve().then(() =>1); // 返回值為 new Promise(resolve => resolve(1))
Promise.resolve().then(() =>Promise.resolve(2)); // 返回 new Promise(resolve => resolve(Promise.resolve(2)))
Promise.resolve().then(() => {
thrownewError('abc')
}); // 返回 new Promise(resolve => resolve(Promise.reject(new Error('abc'))))
Promise.reject().then(() =>1, () = 2); // 返回值為 new Promise(resolve => resolve(2))
// catch函式
Promise.reject().catch(() =>3); // 返回值為 new Promise(resolve => resolve(3))
Promise.resolve().catch(() =>4); // 返回值為 new Promise(resolve => resolve(呼叫catch的promise物件))
// finally函式
// 以下返回值均為 new Promise(resolve => resolve(呼叫finally的promise物件))
Promise.resolve().finally(() => {});
Promise.reject().finally(() => {});
7. then 函式的第二個回呼和 catch 回呼有什麽不同?
promise 的 then 的第二個回呼函式和 catch 在請求出錯時都會被觸發,咋一看沒什麽區別啊,但其實,前者不能捕獲當前 then 第一個回呼函式中丟擲的錯誤,但 catch 可以。
Promise.resolve().then(
() => {
thrownewError('來自成功回呼的錯誤');
},
() => {
// 不會被執行
}
).catch(reason => {
console.log(reason.message); // 將打印出"來自成功回呼的錯誤"
});
其原理也正如於上一點所言,catch 函式是在 then 函式返回的 rejected 狀態的 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 行左右的程式碼簡單實作了一番,如有與 koa 雷同,純屬巧合。
接下來我們分析一番
註意:以下內容對新手不太友好,請斟酌觀看。
首先將中介軟體函式先保存起來,並在 listen 函式中接收到請求後就呼叫洋蔥模型的執行。
functionaction(koaInstance, ctx) {
// ...
}
classKoa{
middlewares = [];
use(mid) {
this.middlewares.push(mid);
}
listen(port) {
// 虛擬碼模擬接收請求
http.on('request', ctx => {
action(this, ctx);
});
}
}
在接收到請求後,先從第一個中介軟體開始序列執行 next 前的前置邏輯。
// 開始啟動中介軟體呼叫
functionaction(koaInstance, ctx) {
let nextMiddlewareIndex = 1; // 標識下一個執行的中介軟體索引
// 定義next函式
functionnext() {
// 剝洋蔥前,呼叫next則呼叫下一個中介軟體函式
const nextMiddleware = middlewares[nextMiddlewareIndex];
if (nextMiddleware) {
nextMiddlewareIndex++;
nextMiddleware(ctx, next);
}
}
// 從第一個中介軟體函式開始執行,並將ctx和next函式傳入
middlewares[0](ctx, next);
}
處理 next 之後的後置邏輯
functionaction(koaInstance, ctx) {
let nextMiddlewareIndex = 1;
functionnext() {
const nextMiddleware = middlewares[nextMiddlewareIndex];
if (nextMiddleware) {
nextMiddlewareIndex++;
// 這邊也添加了return,讓中介軟體函式的執行用promise從後到前串聯執行(這個return建議反復理解)
returnPromise.resolve(nextMiddleware(ctx, next));
} else {
// 當最後一個中介軟體的前置邏輯執行完後,返回fullfilled的promise開始執行next後的後置邏輯
returnPromise.resolve();
}
}
middlewares[0](ctx, next);
}
到此,一個簡單的洋蔥模型就實作了。
參考資料
[1]
https://alova.js.org/tutorial/next-step/share-request:
https://link.juejin.cn?target=https://alova.js.org/tutorial/next-step/share-request
本文轉載自:https://juejin.cn/post/7263089207128850489 作者:古韻
結語
我是林三心
一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;
一個偏前端的全幹工程師;
一個不正經的金塊作者;
逗比的B站up主;
不帥的小紅書博主;
喜歡打鐵的籃球菜鳥;
喜歡歷史的乏味少年;
喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 -->