当前位置: 欣欣网 > 码农

分享八个 Promise 高级用法,完全掌握 Promise !

2024-05-30码农

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~

在 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 => {
// ...
});

上面两个请求其实只会真正发出一次,并且同时收到相同的响应值。

那么,请求共享会有哪几个使用场景呢?我认为有以下三个:

  1. 当一个页面同时渲染多个内部自获取数据的组件时;

  2. 提交按钮未被禁用,用户连续点击了多次提交按钮;

  3. 在预加载数据的情况下,还未完成预加载就进入了预加载页面;

这也是 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,
dataJSON.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 雷同,纯属巧合。

接下来我们分析一番

注意:以下内容对新手不太友好,请斟酌观看。

  1. 首先将中间件函数先保存起来,并在 listen 函数中接收到请求后就调用洋葱模型的执行。

functionaction(koaInstance, ctx{
// ...
}
classKoa{
middlewares = [];
use(mid) {
this.middlewares.push(mid);
}
listen(port) {
// 伪代码模拟接收请求
http.on('request', ctx => {
action(this, ctx);
});
}
}

  1. 在接收到请求后,先从第一个中间件开始串行执行 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);
}

  1. 处理 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多名前端小伙伴在等着一起学习哦 -->