整会promise这10个高级用法,再被问倒来喷我

本文来自alova作者投稿,alova可以让你编写更少的代码即可实现特定业务场景下的高效数据交互,在过去3个月对外发布以来在github上已收到了 1500+star,alova官网,感兴趣可以看看。

发现很多人还只会promise常规用法

在js项目中,promise的使用应该是必不可少的,但我发现在同事和面试者中,很多中级或以上的前端都还停留在promiseInst.then()promiseInst.catch()Promise.all等常规用法,连async/await也只是知其然,而不知其所以然。

但其实,promise还有很多巧妙的高级用法,也将一些高级用法在alova请求策略库内部大量运用。

现在,我把这些毫无保留地在这边分享给大家,看完你应该再也不会被问倒了,最后还有压轴题哦

1. promise数组串行执行

例如你有一组接口需要串行执行,首先你可能会想到使用await

javascript 复制代码
const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
for (const requestItem of requestAry) {
  await promiseItem();
}

如果使用promise的写法,那么你可以使用then函数来串联多个promise,从而实现串行执行。

js 复制代码
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为例来看看下面的示例。

html 复制代码
<!-- 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即可使用模态框,轻松获取用户填写的数据。

html 复制代码
<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的函数,例如下面两个函数是等价的:

js 复制代码
const fn1 = async () => 1;
const fn2 = () => Promise.resolve(1);

fn1(); // 也返回一个值为1的promise对象

await在大部分情况下在后面接promise对象,并等待它成为fullfilled状态,因此下面的fn1函数等待也是等价的:

js 复制代码
await fn1();

const promiseInst = fn1();
await promiseInst;

然而,await还有一个鲜为人知的秘密,当后面跟的是非promise对象的值时,它会将这个值使用promise对象包装,因此await后的代码一定是异步执行的。如下示例:

js 复制代码
Promise.resolve().then(() => {
  console.log(1);
});
await 2;
console.log(2);
// 打印顺序位:1  2

等价于

js 复制代码
Promise.resolve().then(() => {
  console.log(1);
});
Promise.resolve().then(() => {
  console.log(2);
});

4. promise实现请求共享

当一个请求已发出但还未响应时,又发起了相同请求,就会造成了请求浪费,此时我们就可以将第一个请求的响应共享给第二个请求。

js 复制代码
request('GET', '/test-api').then(response1 => {
  // ...
});
request('GET', '/test-api').then(response2 => {
  // ...
});

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

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

  1. 当一个页面同时渲染多个内部自获取数据的组件时;
  2. 提交按钮未被禁用,用户连续点击了多次提交按钮;
  3. 在预加载数据的情况下,还未完成预加载就进入了预加载页面;

这也是alova的高级功能之一,实现请求共享需要用到promise的缓存功能,即一个promise对象可以通过多次await获取到数据,简单的实现思路如下:

js 复制代码
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最终是什么状态?

js 复制代码
const promise = new Promise((resolve, reject) => {
  resolve();
  reject();
});

正确答案是fullfilled状态,我们只需要记住,promise一旦从pending状态转到另一种状态,就不可再更改了,因此示例中先被转到了fullfilled状态,再调用reject()也就不会再更改为rejected状态了。

6. 彻底理清then/catch/finally返回值

先总结成一句话,就是以上三个函数都会返回一个新的promise包装对象,被包装的值为被执行的回调函数的返回值,回调函数抛出错误则会包装一个rejected状态的promise。,好像不是很好理解,我们来看看例子:

js 复制代码
// 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可以。

js 复制代码
Promise.resolve().then(
  () => {
    throw new Error('来自成功回调的错误');
  },
  () => {
    // 不会被执行
  }
).catch(reason => {
  console.log(reason.message); // 将打印出"来自成功回调的错误"
});

其原理也正如于上一点所言,catch函数是在then函数返回的rejected状态的promise上调用的,自然也就可以捕获到它的错误。

8. (压轴)promise实现koa2洋葱中间件模型

koa2框架引入了洋葱模型,可以让你的请求像剥洋葱一样,一层层进入再反向一层层出来,从而实现对请求统一的前后置处理。

我们来看一个简单的koa2洋葱模型:

js 复制代码
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函数中接收到请求后就调用洋葱模型的执行。
js 复制代码
function action(koaInstance, ctx) {
  // ...
}

class Koa {
  middlewares = [];
  use(mid) {
    this.middlewares.push(mid);
  }
  listen(port) {
    // 伪代码模拟接收请求
    http.on('request', ctx => {
      action(this, ctx);
    });
  }
}
  1. 在接收到请求后,先从第一个中间件开始串行执行next前的前置逻辑。
js 复制代码
// 开始启动中间件调用
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之后的后置逻辑
js 复制代码
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);
}

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

结尾

各位看官们,要是觉得对你有用请帮忙点个赞或收藏,然后还有哪里不理解的或更高级的用法吗?请在评论区说出你的想法!

相关信息

我是alova御用推广人🤘🏻🤘🏼🤘,alova是一个轻量级的请求策略库,它针对不同请求场景分别提供了具有针对性的请求策略,来提升应用可用性、流畅性,降低服务端压力,让应用如智者一般具备卓越的策略思维。

alova官网

alova Github地址

往期热文

相关推荐
滚雪球~1 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
m0_748234522 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
噢,我明白了2 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__3 小时前
APIs-day2
javascript·css·css3
关你西红柿子3 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根3 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.3 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia3114 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
m0_748256564 小时前
Vue - axios的使用
前端·javascript·vue.js