Promise基础

42 阅读7分钟

本文章部分参考Promise,添加了自己的理解,让读者更好快速理解promise

Promise简介

什么是promise,这里使用类比的方法解释:

你走进汉堡店,买了一个汉堡,过了一会儿,你收到了你的汉堡

// 传统回调方式
function orderBurger(callback) {
    console.log('下单汉堡...');
    setTimeout(() => { callback('🍔 你的汉堡'); }, 3000); 
}
orderBurger((burger) => { console.log('拿到了:', burger); });

看起来很简单,但是如果情况变得复杂起来呢?

你拿到了你的汉堡,此时你决定再点一份薯条,收到薯条以后,你又决定再点一杯可乐。 在传统的回调方式中,你需要在下单汉堡成功的情况里面嵌套下单薯条的两种情况,再在下单薯条成功的情况里面嵌套下单可乐的两种情况,就像这样:

function orderBurger(callback) {
  console.log("🍔 正在制作汉堡...");
  setTimeout(() => {
    callback(null, "🍔 你的汉堡");
  }, 2000);
}

function orderFries(callback) {
  console.log("🍟 正在炸薯条...");
  setTimeout(() => {
    callback(null, "🍟 你的薯条");
  }, 1500);
}

function orderCoke(callback) {
  console.log("🥤 正在倒可乐...");
  setTimeout(() => {
    callback(null, "🥤 你的可乐");
  }, 1000);
}

// 使用回调(越来越深的嵌套)
console.log("👋 欢迎光临!开始点餐...\n");

orderBurger((error, burger) => {
  if (error) {
    console.log("❌ 汉堡出错:", error);
    return;
  }
  console.log("✅ 拿到了:", burger);
  console.log("");

  // 第一层嵌套
  orderFries((error, fries) => {
    if (error) {
      console.log("❌ 薯条出错:", error);
      return;
    }
    console.log("✅ 拿到了:", fries);
    console.log("");

    // 第二层嵌套 - 回调地狱开始!
    orderCoke((error, coke) => {
      if (error) {
        console.log("❌ 可乐出错:", error);
        return;
      }
      console.log("✅ 拿到了:", coke);
      console.log("");
      console.log("🎉 所有食物都拿到了,开始享用!");

      // 如果还想点甜品... 继续嵌套 😱
    });
  });
});

随着情况越来越复杂,嵌套回越来越深,形成回调地狱。

而使用promise,就会变成这种扁平的结构,方便修改和debug。

// 1️⃣ 点汉堡
function orderBurger() {
  console.log("🍔 正在制作汉堡...");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🍔 你的汉堡");
    }, 2000);
  });
}

// 2️⃣ 点薯条
function orderFries() {
  console.log("🍟 正在炸薯条...");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🍟 你的薯条");
    }, 1500);
  });
}

// 3️⃣ 点可乐
function orderCoke() {
  console.log("🥤 正在倒可乐...");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("🥤 你的可乐");
    }, 1000);
  });
}

console.log("👋 欢迎光临!开始点餐...\n");

// 方式1: Promise 链式调用(推荐)
orderBurger()
  .then((burger) => {
    console.log("✅ 拿到了:", burger);
    console.log("");
    return orderFries(); // 返回新的 Promise
  })
  .then((fries) => {
    console.log("✅ 拿到了:", fries);
    console.log("");
    return orderCoke(); // 返回新的 Promise
  })
  .then((coke) => {
    console.log("✅ 拿到了:", coke);
    console.log("");
    console.log("🎉 所有食物都拿到了,开始享用!");
  })
  .catch((error) => {
    console.log("❌ 出错了:", error);
  });

好了,我们系统地解读一下promise的语法:promise分成两个部分,生产者代码和消费者代码。

  1. “生产者代码(producing code)”会做一些事儿,并且会需要一些时间。例如,通过网络fetch数据。
  2. “消费者代码(consuming code)”想要在“生产者代码”完成工作的第一时间就能获得其工作成果。许多函数可能都需要这个结果。

Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。


生产者

Promise 对象的构造器(constructor)语法如下:

let promise = new Promise((resolve, reject)=> {
  // 执行生产者代码
});

传递给 new Promise 的函数被称为 executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。

它的参数 resolve 和 reject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部。可以理解为执行成功的消息如何传递给处理成功的消费者 以及 执行失败后的报错如何传给处理报错的消费者 都是已经被js处理好了,我们这里只管执行(以及报告结果)。

new Promise((resolve, reject) => {
  // ← 生产者区域(你的代码)
  // 你在这里生产结果

  if (成功) {
    resolve(结果); // 把结果交给 JS 引擎
  } else {
    reject(错误); // 把错误交给 JS 引擎
  }
})

  // ← JS 引擎的工作区(自动处理)
  // JS 会自动把 resolve 的结果传给 .then
  // JS 会自动把 reject 的错误传给 .catch

  .then((结果) => {
    // ← 消费者区域(你的代码)
    // 你在这里使用结果
  })
  .catch((错误) => {
    // ← 消费者区域(你的代码)
    // 你在这里处理错误
  });

executor 会自动运行并尝试执行一项工作。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject

由 new Promise 构造器返回的 promise 对象具有以下内部属性:

  • state —— 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"
  • result —— 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error

所以,executor 最终将 promise 移至以下状态之一:

image.png


下面是一个 promise 构造器和一个简单的 executor 函数,该 executor 函数具有包含时间(即 setTimeout)的“生产者代码”:

let promise = new Promise(function(resolve, reject) {
  // 当 promise 被构造完成时,自动执行此函数

  // 1 秒后发出工作已经被完成的信号,并带有结果 "done"
  setTimeout(() => resolve ("done") , 1000);
});

通过运行上面的代码,我们可以看到两件事儿:

  1. executor 被自动且立即调用(通过 new Promise)。

  2. executor 接受两个参数:resolve 和 reject。这些函数由 JavaScript 引擎预先定义,因此我们不需要创建它们。我们只需要在准备好(译注:指的是 executor 准备好)时调用其中之一即可。

    经过 1 秒的“处理”后,executor 调用 resolve("done") 来产生结果。这将改变 promise 对象的状态:

这是一个成功完成任务的例子,一个“成功实现了的诺言”。

下面则是一个 executor 以 error 拒绝 promise 的示例:

let promise = new Promise(function(resolve, reject) {
  // 1 秒后发出工作已经被完成的信号,并带有 error
  setTimeout(() => reject (new Error("Whoops!")) , 1000);
});

对 reject(...) 的调用将 promise 对象的状态移至 "rejected"

总而言之,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolve 或 reject 来改变对应的 promise 对象的状态。

与最初的 “pending” promise 相反,一个 resolved 或 rejected 的 promise 都会被称为 “settled”。


  • 只能有一个结果

executor 任何状态的更改都是不可再次改变的

所有其他的再对 resolve 和 reject 的调用都会被忽略:

let promise = new Promise(function(resolve, reject) {
resolve("done");

  reject(new Error("…")); // 被忽略
  setTimeout(() => resolve("…")); // 被忽略
});
  • 以 Error 对象 reject

如果什么东西出了问题,executor 应该调用 reject。这可以使用任何类型的参数来完成(就像 resolve 一样)。但建议使用 Error 对象(或继承自 Error 的对象)。

  • resolve/reject 可以立即进行

实际上,executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的。我们还可以立即调用 resolve 或 reject,就像这样:

let promise = new Promise(function(resolve, reject) {
  // 不花时间去做这项工作
  resolve(123); // 立即给出结果:123
});

例如,当我们开始做一个任务,随后发现一切都已经完成并已被缓存时,可能就会发生这种情况。

这挺好。我们立即就有了一个 resolved 的 promise。

  • state 和 result 都是内部的

Promise 对象的 state 和 result 属性都是内部的。我们无法直接访问它们。但我们可以对它们使用 .then/.catch/.finally 方法。我们在下面对这些方法进行了描述。


消费者:then,catch

消费者可以通过使用 .then 和 .catch 方法注册消费函数的结果。

[then]处理成功结果,[catch]处理失败结果,很好理解吧

清理者:finally(没有这个名词,我自己起的)

finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。

例如,停止加载指示器,关闭不再需要的连接等。

把它想象成派对的终结者。无论派对是好是坏,有多少朋友参加,我们都需要(或者至少应该)在它之后进行清理。

new Promise((resolve, reject) => {
  /* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
// 在 promise 为 settled 时运行,无论成功与否
  .finally(() => stop loading indicator)
  // 所以,加载指示器(loading indicator)始终会在我们继续之前停止
  .then(result => show result, err => show error)

注意

  1. finally 处理程序(handler)没有参数。在 finally 中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。

  2. finally 不消费结果,它只会将结果“传递”给下一个合适的处理程序。

    例如,在这结果被从 finally 传递给了 then

    new Promise((resolve, reject) => {
      setTimeout(() => resolve("value"), 2000)
    })
      .finally(() => alert("Promise ready")) // 先触发
      .then(result => alert(result)); // <-- .then 显示 "value"
    

    正如我们所看到的,第一个 promise 返回的 value 通过 finally 没被消费,被传递给了下一个 then