Promise:避免回调地狱的异步处理方案

0 阅读5分钟

注:本文是学习事件循环后的个人笔记,建议配合以下参考资料一起阅读。

资料来自


什么是Promise

首先需要明确什么是异步操作:异步操作是指不会立刻返回结果的操作,比如网络请求需要时间,JavaScript 不能傻等,会继续执行后面的代码,等结果回来了再处理。

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。 它接受一个回调函数,称之为executor,参数为resolve和reject,前者用于修改当前promise对象为成功状态(fulfilled),后者则是修改为失败状态(rejected)。

修改为成功状态

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

修改为失败状态

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

为什么有 Promise

使用Promises会带来两个好处

  1. 避免回调地狱
  2. Promise创建和Promise处理的解耦

接下来会详细说明

回调地狱

promise提供了一种链式的结构,用于解决之前js存在的因为多层回调嵌套导致的回调地狱情况

doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log(`得到最终结果:${finalResult}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

随着嵌套层级和代码复杂度的提升,阅读难度会很高。

使用promise,则能改写成

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`得到最终结果:${finalResult}`);
  })
  .catch(failureCallback);

Promise改写了返回值的传递方式——原版是通过嵌套来层层传递,Promise 是通过返回值来传递。可以看到通过promise的改写,降低了嵌套的层级,提升了代码的可读性,即使有很多的.then,阅读顺序也是向下,且每次处理的操作是很清晰的。

Promise创建和Promise处理的解耦

若采用旧式的回调写法,则创建和处理是立刻发生的,无法做到先创建,然后在指定的时间处理

// 成功的回调函数
function successCallback(result) {
  console.log("音频文件创建成功:" + result);
}

// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败:" + error);
}

// 传入处理成功的函数和失败的函数,然后直接执行
createAudioFileAsync(audioSettings, successCallback, failureCallback);

采用Promise,则能实现二者的解耦

const audioPromise = createAudioFileAsync(audioSettings);

// 渲染 UI、处理别的操作等

// 可以在任意时刻、任意地方绑定处理函数
function successCallback(result) {
  console.log("音频文件创建成功:" + result);
}
// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败:" + error);
}
audioPromise.then(successCallback, failureCallback);

如何使用 Promise

.then

.then是对Promise对象的具体操作函数,他接受两个回调函数,分别为onFulfilledonRejected——前者用于处理fulfilled的Promise对象,后者则是rejected的Promise对象。

.then支持链式写法,它接受一个promise,根据promise状态处理它,然后包装成新的Promise传递给下一个.then

doSomething()
  .then((url) => fetch(url))
  .then((res) => res.json())
  .then((data) => {
    listOfIngredients.push(data);
  })
  .then(() => {
    console.log(listOfIngredients);
  });

.catch 与错误处理

.then具有穿透性,若一条.then链没有用于处理rejected的Promise对象,则会让错误一路传递下去,最后静默消失。

为了避免这种情况,引入了catch。它是用于处理rejected的Promise对象的函数,fulfilled 的 Promise 会跳过 .catch(),直接传给下一个 .then()。catch处理后,会返回fulfilled的Promise对象,但若未能处理,则错误会继续向下穿透。

注:catch的写法等价于.then(null, fn)

例:catch处理掉错误后

Promise.reject("出错了")
  .catch((err) => {
    console.log("捕获到错误:", err); // 捕获到错误:出错了
    // 正常执行完,没有抛出新错误
  })
  .then(() => {
    console.log("链继续执行了"); // 这里会执行
  });

输出

捕获到错误:出错了
链继续执行了

例:catch未能处理错误

Promise.reject("出错了")
  .catch((err) => {
    throw new Error("catch里又出错了");
  })
  .then(() => {
    console.log("这里不会执行");
  })
  .catch((err) => {
    console.log("这里才会执行:", err.message); // 这里才会执行:catch里又出错了
  });

.all

用于同时执行多个异步函数,但其中一个失败则全部失败。.then() 收到的参数是一个数组,顺序跟传入的顺序一致,不管哪个先完成。

Promise.all([func1(), func2(), func3()])
	.then(([result1, result2, result3]) => {
	/* 使用 result1、result2 和 result3 */
	});

.allSettled

也是同时用于处理多个异步函数,但区别在于任意一个失败都不会导致失败。返回一个数组,需要手动解析每一个Promise情况。

Promise.allSettled([func1(), func2(), func3()]).then((results) => {
    // results 是一个数组,每项长这样:
    // { status: "fulfilled", value: ... }  // 成功
    // { status: "rejected", reason: ... }  // 失败
});

async/await

async/await是用于简化Promise和then的语法糖,他能用同步的写法来写异步函数。

其中外层函数用async关键字标记为异步函数,函数体则可使用await关键字,使用await标记的异步函数会自动拆开 Promise,拿到里面的值。

以之前的doSomething函数为例

doSomething()
  .then((url) => fetch(url))
  .then((res) => res.json())
  .then((data) => {
    listOfIngredients.push(data);
  })
  .then(() => {
    console.log(listOfIngredients);
  });

改写成async/await的写法后则是

async function logIngredients() {
  const url = await doSomething();
  const res = await fetch(url);
  const data = await res.json();
  listOfIngredients.push(data);
  console.log(listOfIngredients);
}

这种写法还带来了可共享作用域的好处:Promise.then的写法中每一个.then的作用域是隔离的,而改写后则可共享。

时序

new Promise 的 executor 是同步执行的,会立刻执行,而.then是微任务

const promise = new Promise((resolve, reject) => {
  console.log("Promise 执行函数");
  resolve();
}).then((result) => {
  console.log("Promise 回调(.then)");
});

setTimeout(() => {
  console.log("新一轮事件循环:Promise(已完成)", promise);
}, 0);

console.log("Promise(队列中)", promise);

输出

Promise 执行函数
Promise(队列中)Promise {<pending>}
Promise 回调(.then)
新一轮事件循环:Promise(已完成)Promise {<fulfilled>}

事件循环/任务队列的内容可参考:事件循环入门:单线程语言如何实现多线程编程