记录--整会promise这8个高级用法
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
发现很多人还只会promise常规用法
在js项目中,promise的使用应该是必不可少的,但我发现在同事和面试者中,很多中级或以上的前端都还停留在promiseInst.then()
、promiseInst.catch()
、Promise.all
等常规用法,连async/await
也只是知其然,而不知其所以然。
但其实,promise还有很多巧妙的高级用法,也将一些高级用法在alova请求策略库内部大量运用。
现在,我把这些毫无保留地在这边分享给大家,看完你应该再也不会被问倒了,最后还有压轴题哦。
觉得对你有帮助还请点赞收藏评论哦!
1. promise数组串行执行
例如你有一组接口需要串行执行,首先你可能会想到使用await
1 2 3 4 | const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()]; for ( const requestItem of requestAry) { await promiseItem(); } |
如果使用promise的写法,那么你可以使用then函数来串联多个promise,从而实现串行执行。
1 2 3 4 5 | 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为例来看看下面的示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <!-- 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
即可使用模态框,轻松获取用户填写的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <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的函数,例如下面两个函数是等价的:
1 2 3 4 | const fn1 = async () => 1; const fn2 = () => Promise.resolve(1); fn1(); // 也返回一个值为1的promise对象 |
而await
在大部分情况下在后面接promise对象,并等待它成为fullfilled状态,因此下面的fn1函数等待也是等价的:
1 2 3 4 | await fn1(); const promiseInst = fn1(); await promiseInst; |
然而,await还有一个鲜为人知的秘密,当后面跟的是非promise对象的值时,它会将这个值使用promise对象包装,因此await后的代码一定是异步执行的。如下示例:
1 2 3 4 5 6 | Promise.resolve().then(() => { console.log(1); }); await 2; console.log(2); // 打印顺序位:1 2 |
等价于
1 2 3 4 5 6 | Promise.resolve().then(() => { console.log(1); }); Promise.resolve().then(() => { console.log(2); }); |
4. promise实现请求共享
当一个请求已发出但还未响应时,又发起了相同请求,就会造成了请求浪费,此时我们就可以将第一个请求的响应共享给第二个请求。
1 2 3 4 5 6 | request( 'GET' , '/test-api' ).then(response1 => { // ... }); request( 'GET' , '/test-api' ).then(response2 => { // ... }); |
上面两个请求其实只会真正发出一次,并且同时收到相同的响应值。
那么,请求共享会有哪几个使用场景呢?我认为有以下三个:
- 当一个页面同时渲染多个内部自获取数据的组件时;
- 提交按钮未被禁用,用户连续点击了多次提交按钮;
- 在预加载数据的情况下,还未完成预加载就进入了预加载页面;
这也是alova的高级功能之一,实现请求共享需要用到promise的缓存功能,即一个promise对象可以通过多次await获取到数据,简单的实现思路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const pendingPromises = {}; function request(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最终是什么状态?
1 2 3 4 | const promise = new Promise((resolve, reject) => { resolve(); reject(); }); |
正确答案是fullfilled
状态,我们只需要记住,promise一旦从pending
状态转到另一种状态,就不可再更改了,因此示例中先被转到了fullfilled
状态,再调用reject()
也就不会再更改为rejected
状态了。
6. 彻底理清then/catch/finally返回值
先总结成一句话,就是以上三个函数都会返回一个新的promise包装对象,被包装的值为被执行的回调函数的返回值,回调函数抛出错误则会包装一个rejected状态的promise。,好像不是很好理解,我们来看看例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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可以。
1 2 3 4 5 6 7 8 9 10 | Promise.resolve().then( () => { throw new Error( '来自成功回调的错误' ); }, () => { // 不会被执行 } ). catch (reason => { console.log(reason.message); // 将打印出"来自成功回调的错误" }); |
其原理也正如于上一点所言,catch函数是在then函数返回的rejected状态的promise上调用的,自然也就可以捕获到它的错误。
8. (压轴)promise实现koa2洋葱中间件模型
koa2框架引入了洋葱模型,可以让你的请求像剥洋葱一样,一层层进入再反向一层层出来,从而实现对请求统一的前后置处理。
我们来看一个简单的koa2洋葱模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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函数中接收到请求后就调用洋葱模型的执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function action(koaInstance, ctx) { // ... } class Koa { middlewares = []; use(mid) { this .middlewares.push(mid); } listen(port) { // 伪代码模拟接收请求 http. on ( 'request' , ctx => { action( this , ctx); }); } } |
- 在接收到请求后,先从第一个中间件开始串行执行next前的前置逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 开始启动中间件调用 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); } |
- 处理next之后的后置逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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); } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-08-04 Vue单点登录控件代码分享