发现很多人还只会 promise 惯例用法在 js 我的项目中,promise 的应用应该是必不可少的,但我发现在共事和面试者中,很多中级或以上的前端都还停留在 promiseInst.then()、promiseInst.catch()、Promise.all 等惯例用法,连 async/await 也只是知其然,而不知其所以然。但其实,promise 还有很多奇妙的高级用法,也将一些高级用法在 alova 申请策略库外部大量使用。当初,我把这些毫无保留地在这边分享给大家,看完你应该再也不会被问倒了,最初还有压轴题哦。8 个例子 1. promise 数组串行执行例如你有一组接口须要串行执行,首先你可能会想到应用 awaitconst 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 => { // …}); 下面两个申请其实只会真正收回一次,并且同时收到雷同的响应值。那么,申请共享会有哪几个应用场景呢?我认为有以下三个:当一个页面同时渲染多个外部自获取数据的组件时;提交按钮未被禁用,用户间断点击了屡次提交按钮;在预加载数据的状况下,还未实现预加载就进入了预加载页面;这也是 alova 的高级性能之一,实现申请共享须要用到 promise 的缓存性能,即一个 promise 对象能够通过屡次 await 获取到数据,简略的实现思路如下: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 最终是什么状态?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 雷同,纯属巧合。接下来咱们剖析一番留神:以下内容对老手不太敌对,请斟酌观看。首先将中间件函数先保存起来,并在 listen 函数中接管到申请后就调用洋葱模型的执行。function action(koaInstance, ctx) {// …}class Koa {middlewares = []; use(mid) {this.middlewares.push(mid); } listen(port) {// 伪代码模仿接管申请 http.on(‘request’, ctx => { action(this, ctx); }); }} 在接管到申请后,先从第一个中间件开始串行执行 next 前的前置逻辑。// 开始启动中间件调用 function action(koaInstance, ctx) {let nextMiddlewareIndex = 1; // 标识下一个执行的中间件索引 // 定义 next 函数 function next() {// 剥洋葱前,调用 next 则调用下一个中间件函数 const nextMiddleware = middlewares[nextMiddlewareIndex]; if (nextMiddleware) {nextMiddlewareIndex++; nextMiddleware(ctx, next); } } // 从第一个中间件函数开始执行,并将 ctx 和 next 函数传入 middlewares0;} 解决 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(); } } middlewares0;} 到此,一个简略的洋葱模型就实现了。