8 个 Promise 高级用法

在 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>用户姓名:<input v-model="info.name" /></div><!-- 其他信息 --><button @click="handleCancel">取消</button><button @click="handleConfirm">提交</button></div><!-- 页面组件 -->
</template><script setup>
import { provide } from 'vue';const visible = ref(false);
const info = reactive({name: ''
});
let resolveFn, rejectFn;// 将信息收集函数函数传到下面
provide('getInfoByModal', () => {visible.value = true;return new Promise((resolve, reject) => {// 将两个函数赋值给外部,突破promise作用域resolveFn = resolve;rejectFn = reject;});
})const handleConfirm = info => {resolveFn && resolveFn(info);
};
const handleCancel = () => {rejectFn && rejectFn(new Error('用户已取消'));
};
</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);
});
await 2;
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 = {};
function request(type, url, data) {// 使用请求信息作为唯一的请求key,缓存正在请求的promise对象// 相同key的请求将复用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;
}

5. 同时调用 resolve 和 reject 会怎么样?

大家都知道 promise 分别有pending/fullfilled/rejected三种状态,但例如下面的示例中,promise 最终是什么状态?

const promise = new Promise((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(() => {throw new Error('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(() => {throw new Error('来自成功回调的错误');},() => {// 不会被执行}
).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 函数中接收到请求后就调用洋葱模型的执行。

function action(koaInstance, ctx) {// ...
}class Koa {middlewares = [];use(mid) {this.middlewares.push(mid);}listen(port) {// 伪代码模拟接收请求http.on('request', ctx => {action(this, ctx);});}
}
  1. 在接收到请求后,先从第一个中间件开始串行执行 next 前的前置逻辑。

// 开始启动中间件调用
function action(koaInstance, ctx) {let nextMiddlewareIndex = 1; // 标识下一个执行的中间件索引// 定义next函数function next() {// 剥洋葱前,调用next则调用下一个中间件函数const nextMiddleware = middlewares[nextMiddlewareIndex];if (nextMiddleware) {nextMiddlewareIndex++;nextMiddleware(ctx, next);}}// 从第一个中间件函数开始执行,并将ctx和next函数传入middlewares[0](ctx, next);
}
  1. 处理 next 之后的后置逻辑

function action(koaInstance, ctx) {let nextMiddlewareIndex = 1;function next() {const nextMiddleware = middlewares[nextMiddlewareIndex];if (nextMiddleware) {nextMiddlewareIndex++;// 这边也添加了return,让中间件函数的执行用promise从后到前串联执行(这个return建议反复理解)return Promise.resolve(nextMiddleware(ctx, next));} else {// 当最后一个中间件的前置逻辑执行完后,返回fullfilled的promise开始执行next后的后置逻辑return Promise.resolve();}}middlewares[0](ctx, next);
}

到此,一个简单的洋葱模型就实现了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/127726.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

完美解决 flex 实现一行三个,显示多行,左对齐

效果图 代码 <body><section class"content"><div class"item">元素</div><div class"item">元素</div><div class"item">元素</div><div class"item">元素</di…

用于现场仪表过程控制的多协议配置工具

一 挑战 在现代生产环境中&#xff0c;我们会使用各种现场设备来记录过程数据&#xff08;从温度传感器到压力、液位和流量计&#xff09;&#xff0c;而这些数据需要通过多种通信协议来进行传输&#xff0c;例如HART、FOUNDATION Fieldbus或PROFIBUS PA。为此&#xff0c;每个…

701. 二叉搜索树中的插入操作

给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 &#xff0c;新值和原始二叉搜索树中的任意节点值都不同。 注意&#xff0c;可能存在多种有效的插入方式&a…

Jenkins配置钉钉通知

Jenkins 作为最流行的开源持续集成平台&#xff0c;其强大的拓展功能一直备受测试人员及开发人员的青睐。大家都知道我们可以在 Jenkins 中安装 Email 插件支持构建之后通过邮件将结果及时通知到相关人员。 但其实 Jenkins 还可以支持钉钉消息通知&#xff0c;其主要通过 Ding…

【Kubernetes】当K8s出现问题时,我们可以从哪些方面排查出

前言 kubernetes&#xff0c;简称K8s&#xff0c;是用8代替名字中间的8个字符“ubernete”而成的缩写。是一个开源的&#xff0c;用于管理云平台中多个主机上的容器化的应用&#xff0c;Kubernetes的目标是让部署容器化的应用简单并且高效&#xff08;powerful&#xff09;,Kub…

【网络安全-sql注入(5)】sqlmap以及几款自动化sql注入工具的详细使用过程(提供工具)

一&#xff0c;sqlmap 工具的详细使用 kali系统自带这个工具&#xff0c;无需安装直接sqlmap 后面接参数使用 Windows上参照以下方法安装即可 1-1 工具下载 1-1-1 sqlmap下载 sqlmap 工具下载地址&#xff1a; GitHub - sqlmapproject/sqlmap: Automatic SQL injection a…

【Java接口性能优化】skywalking使用

skywalking使用 提示&#xff1a;微服务中-skywalking使用 文章目录 skywalking使用一、进入skywalking主页二、进入具体服务1.查看接口 一、进入skywalking主页 二、进入具体服务 可以点击列表或搜索后&#xff0c;点击进入具体服务 依次选择日期、小时、分钟 1.查看接口 依次…

Android stdio的Gradle菜单栏无内容问题的解决方法

右边Gradle菜单栏里没有Tasks选项内容的问题 正常情况↓ 如果这个问题如果无法解决的话&#xff0c;Gradle打包就只能通过控制台输入命令来解决&#xff0c;但这无疑是把简单问题复杂化了&#xff0c;我们来看看怎么解决这个问题吧。 这里有几个方法提供&#xff0c;可以自行选…

【jvm--堆】

文章目录 1. 堆&#xff08;Heap&#xff09;的核心概述2. 图解对象分配过程2.1 Minor GC&#xff0c;MajorGC、Full GC2.1 堆空间分代思想2.3 内存分配策略2.4 TLAB&#xff08;Thread Local Allocation Buffer&#xff09;2.5 堆空间的参数设置2.6 逃逸分析2.7 逃逸分析&…

Flink学习笔记(二):Flink内存模型

文章目录 1、配置总内存2、JobManager 内存模型3、TaskManager 内存模型4、图形化展示5、实际案例计算内存分配 1、配置总内存 Flink JVM 进程的进程总内存&#xff08;Total Process Memory&#xff09;包含了由 Flink 应用使用的内存&#xff08;Flink 总内存&#xff09;以…

软件安全测试的含义是什么?

早在信息行业发展的初期&#xff0c;就有互联网公司意识到了保护软件产品安全的重要性&#xff0c;可时至今日&#xff0c;我们总能够从报道中瞥见某某公司遭黑客入侵导致信息泄露的新闻&#xff0c;其中甚至不乏Facebook这些以用户通讯、身份信息为支柱产业的大厂。 信息安全事…

实现协议互通:探索钡铼BL124EC的EtherCAT转Ethernet/IP功能

钡铼BL124EC是一种用于工业网络通信的网关设备&#xff0c;专门用于将EtherCAT协议转换成Ethernet/IP协议。它充当一个桥梁&#xff0c;连接了使用不同协议的设备&#xff0c;使它们能够无缝地进行通信和互操作。 具体来说&#xff0c;BL124EC通过支持EtherCAT&#xff08;以太…