掌握8个Promise高级技巧,提升异步编程能力!

目录
文章目录隐藏
  1. 1. 串行执行 Promise 数组
  2. 2. 在新 Promise 作用域外改变状态
  3. 3. async/await 的替代用法
  4. 4. 使用 Promise 实现请求共享
  5. 5. 如果同时调用 resolve 和 reject 会发生什么?
  6. 6. 彻底弄清 then/catch/finally 的返回值
  7. 7. then 函数的第二个回调与 catch 回调有何不同?
  8. 8. 实现 Koa2 洋葱模型中的 Promise
  9. 结语

在 JavaScript 项目中,Promise 的使用是必不可少的。然而,我发现许多中高级前端开发人员仍然停留在常见的 promiseInst.then()、promiseInst.catch()、Promise.all 甚至 async/await 等常规实践上,并没有深入理解它们。

实际上,Promise 有许多巧妙的高级用法,其中一些在 ALOVA 请求策略库中广泛使用。

ALOVA 是一个基于 Promise 的请求策略库,旨在帮助开发者更高效地进行 HTTP 请求处理。它通过高级的 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. 在新 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>

JS 代码

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 = () => {
   resolveFn && resolveFn(info);
};
const handleCancel = () => {
   rejectFn && rejectFn(new Error('用户已取消。'));
};

接下来,直接调用 getInfoByModal 即可使用模态框,并轻松获取用户填写的数据。

<template>
  <button @click="handleClick">填写信息</button>
</template>
import { inject } from 'vue';
const getInfoByModal = inject('getInfoByModal');
const handleClick = async () => {
   // 调用后,会显示模态框。当用户点击“确认”时,Promise 会变为 fulfilled 状态,从而获取用户信息。
   const info = await getInfoByModal();
   await api.submitInfo(info);
}

这也是许多 UI 组件库中封装常用组件的方法。

3. async/await 的替代用法

许多人只知道在调用 async function 时使用 await 接收返回值,但他们不知道 async 函数实际上是返回 Promise 的函数。例如,以下两个函数是等价的:

const fn1 = async () => 1;
const fn2 = () => Promise.resolve(1);
fn1(); // 同样返回一个值为 1 的 Promise 对象。

在大多数情况下,await 后面跟着的是一个 Promise 对象,并等待其变为 fulfilled 状态。因此,下面的函数 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) {
  // 使用请求信息作为唯一请求键来缓存正在请求的 Promise 对象。
  // 具有相同键的请求将重用该 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、fulfilled 和 rejected。但在下面的例子中,Promise 的最终状态是什么?

const promise = new Promise((resolve, reject) => {
  resolve();
  reject();
});

正确答案是 fulfilled 状态。我们只需记住,一旦 Promise 从 pending 状态转变为其他状态,就不能再改变。因此,在这个例子中,调用 resolve()后,即使调用 reject(),状态也不会再改变。

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

总结一句话,上述三个函数都会返回一个新的 Promise 包装对象,包装的值是执行回调函数的返回值。如果回调函数抛出错误,它将包装一个处于 rejected 状态的 Promise。这不太容易理解,我们来看一个例子:

你可以将它们一个一个复制并在浏览器控制台中运行,以更好地理解。

// then 函数
Promise.resolve().then(() => 1); // return new Promise(resolve => resolve(1))
Promise.resolve().then(() => Promise.resolve(2)); // return new Promise(resolve => resolve(Promise.resolve(2)))
Promise.resolve().then(() => {
  throw new Error('abc')
}); // return new Promise(resolve => resolve(Promise.reject(new Error('abc'))))
Promise.reject().then(() => 1, () => 2); // return new Promise(resolve => resolve(2))

// catch 函数
Promise.reject().catch(() => 3); // return new Promise(resolve => resolve(3))
Promise.resolve().catch(() => 4); // 返回值是一个新的 Promise,解析为调用 catch 的 Promise 对象。

// 当 finally 函数的返回值不是 Promise 时,返回 finally 函数之前的 Promise 对象。
Promise.resolve().finally(() => {}); // return Promise.resolve()
Promise.reject().finally(() => {}); // return Promise.reject()
// 当 finally 函数的返回值是 Promise 时,等待返回的 Promise 解析后再返回 finally 函数之前的 Promise 对象。
Promise.resolve(5).finally(() => new Promise(res => {
  setTimeout(res, 1000);
})); // 返回一个处于 pending 状态的 Promise,1 秒后解析为 5。
Promise.reject(6).finally(() => new Promise(res => {
  setTimeout(res, 1000);
})); // 返回一个处于 pending 状态的 Promise,1 秒后抛出数字 6。

7. then 函数的第二个回调与 catch 回调有何不同?

Promise 中的 then 函数的第二个回调和 catch 函数在请求失败时都会被触发。乍一看,它们似乎没有太大区别,但实际上,前者无法捕捉当前 then 函数第一个回调函数中抛出的错误,而 catch 函数可以。

Promise.resolve().then(
  () => {
    throw new Error('成功回调中的错误');
  },
  () => {
    // 不会执行
  }
).catch(reason => {
  console.log(reason.message); // 打印“成功回调中的错误”
});

其原理如前所述,catch 函数是在 then 函数返回的 Promise 处于 rejected 状态时调用的,自然可以捕捉到其错误。

8. 实现 Koa2 洋葱模型中的 Promise

Koa2 框架引入了洋葱模型,使你的请求像剥洋葱一样逐层处理,按相反顺序进入和退出层,从而实现请求的统一前后处理。

实现 Koa2 洋葱模型中的 Promise

我们看看一个简单的 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 函数。
  middlewares[0](ctx, next);
}

处理“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 {
      // 在最后一个中间件的前置逻辑执行完后,返回的 fulfilled Promise 将开始执行 next 之后的后置逻辑。
      return Promise.resolve();
    }
  }
  middlewares[0](ctx, next);
}

结语

在前端开发领域,Promise 是一种处理异步操作的重要工具。除了基本的使用,掌握一些高级技巧能够让你的代码更加优雅和高效。你将能够更好地处理异步操作,提高前端开发的效率和质量。

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系maynote@foxmail.com处理
码云笔记 » 掌握8个Promise高级技巧,提升异步编程能力!

发表回复