Promise

47 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情

前言

promise就像视频博主,每次更新视频都给订阅者发消息提醒类似。好处在于,有新视频可以提醒观看,意外发不了也会有通知。

代码世界

  1. 生产者代码:会做一些事,需要时间。 (博主)
  2. 消费者代码:想“生产者代码”完成工作的第一时间就能获得其工作的成果 (订阅者)
  3. Promise: 将“生产者代码”和“消费者代码”连接起来的JS对象。 (订阅列表)
  • “生产者代码”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在它(译注:指的是“生产者代码”,也就是下文所说的 executor)准备好时,将结果向所有订阅了的代码开放。

生产者世界

语法

    let promise = new Promise(function(resolve,reject){
         // exectutor (生产者代码,"博主")})

解读:

  1. new Promise内部的这个函数————executor。当 new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。按照上面的类比:executor 就是“博主”。

  2. 内部的resolve 和 reject 是由 JavaScript 自身提供的回调(executor的回调)。

  • resolve(value) —— 如果任务成功完成并带有结果 value
  • reject(error) —— 如果出现了 error,error 即为 error 对象。

promise对象的内部属性

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

image.png

简单的使用

成功的情况

    let promise = new Promise(function(resolve,reject){
        
        setTimeout(() => resolve("done"),2000)
     })
  1. executor 被自动执行且立即调用(通过 new Promise)。

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

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

image.png

失败的情况

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

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

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

image.png

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

专一的回调

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

  reject(new Error("…")); // 被忽略
  setTimeout(() => resolve("…")); // 被忽略
});
  • 宗旨是,一个被 executor 完成的工作只能有一个value或一个 error。并且,resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数。

消费者世界

  • 使用 .then 和 .catch 方法注册消费函数。

.then

语法:

promise.then(
  function(result) {  /* handle a successful result */  },
  function(error) {  /* handle an error */  }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。

.then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。

举个例子:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve 运行 .then 中的第一个函数
promise.then(
result => alert(result), // 1 秒后显示 "done!"
  error => alert(error) // 不运行
);

第一个函数被运行了。 在 reject 的情况下,运行第二个:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject 运行 .then 中的第二个函数
promise.then(
  result => alert(result), // 不运行
error => alert(error) // 1 秒后显示 "Error: Whoops!"
);

特别关注

  • 只对成功完成的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:
let promise = new Promise(resolve => {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // 1 秒后显示 "done!" 
  • 只对error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的:
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(alert) 与 promise.then(null, alert) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!" 

.catch(f) 调用是 .then(null, f) 的完全的一致,它只是一个简写形式。

清理

  • finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。 (一个 resolved 或 rejected 的 promise 都会被称为 “settled”。)
new Promise((resolve, reject) => {
  /* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
// 在 promise 为 settled 时运行,无论成功与否
  .finally(() => stop loading indicator)
  // 所以,加载指示器(loading indicator)始终会在我们继续之前停止
  .then(result => show result, err => show error)

请注意,finally(f) 并不完全是 then(f,f) 的别名。它没有参数且不知道promise是否成功,将结果或 error 的promise结果由下面的处理程序处理

应用

接下来,让我们看一下关于 promise 如何帮助我们编写异步代码的更多实际示例。

这是基于回调函数的变体,记住它:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

让我们用 promise 重写它。

新函数 loadScript 将不需要回调。取而代之的是,它将创建并返回一个在加载完成时 resolve 的 promise 对象。外部代码可以使用 .then 向其添加处理程序(订阅函数):

function loadScript(src) {
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Script load error for ${src}`));

    document.head.append(script);
  });
}

用法:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

我们立刻就能发现 promise 相较于基于回调的模式的一些好处:

promisecallback
promise 允许我们按照自然顺序进行编码。首先,我们运行 loadScript 和 .then 来处理结果。在调用 loadScript(script, callback) 时,我们必须有一个 callback 函数可供使用。换句话说,在调用 loadScript 之前,我们必须知道如何处理结果。
我们可以根据需要,在 promise 上多次调用 .then。每次调用,我们都会在“订阅列表”中添加一个新的“粉丝”,一个新的订阅函数。只能有一个回调。

因此,promise 为我们提供了更好的代码流和灵活性。