什么是JavaScript中的 "承诺"?

172 阅读11分钟

几年前,我在学习诺言时曾遇到过理解上的困难。

问题是大多数的教程都只是在描述承诺对象,它的方法等等。但我并不太关心诺言,我关心的是,只要它能让编码变得更简单

下面是我想读的帖子,以便自己理解诺言。这篇帖子描述了为什么承诺能使异步逻辑的编码更容易,然后解释了如何正确使用承诺,包括用async/await 语法。

目录

1.为什么是承诺

JavaScript可以很好地处理命令式和同步式代码。

让我们考虑一个函数findPerson(who) ,它可以确定一个人名是否包含在一个人的列表中。

function getList() {
  return ['Joker', 'Batman'];
}
function findPerson(who) {
  const list = getList();
  const found = list.some(person => person === who);
  console.log(found); // logs true
}
findPerson('Joker');

上面的片段是同步的和阻塞的。当JavaScript进入findPerson() 函数时,直到该函数被执行,它才会离开那里。

获取人员列表const list = getList() ,也是一个同步操作。

同步代码是直截了当的。但你并不总是有运气即时访问数据:有些数据,如通过网络获取数据,可能需要一段时间才能得到。

例如,假设从getList() ,访问人员列表是一个需要,例如,1秒的操作。

function getList() {
  setTimeout(() => {
    ['Joker', 'Batman'] // How to return the list?
  }, 1000);
}
function findPerson(who) {
  const list = /* How to access the list? */;
  const found = list.some(person => person === who);
  console.log(found);
}
findPerson('Joker'); // logs true

如何以1秒的延迟从getList() 返回人员名单?同样,findPerson(who) ,如何访问延迟的名单?

不幸的是,现在事情变得更加复杂了。让我们看看如何对延迟列表进行编码的几个解决方案。

1.1 回调方法

一个经典的方法是引入回调。

function getList(callback) {
  setTimeout(() => callback(['Joker', 'Batman']), 1000);
}
function findPerson(who) {
  getList(list => {
    const found = list.some(person => person === who);
    console.log(found); // logs true
  });
}
findPerson('Joker');

getList(callback) 因为它需要多一个参数:回调函数,所以变得更加复杂。

另外,在findPerson(who) ,你必须提供一个回调getList(list => { ... }) ,以正确地访问列表。

使用回调的代码更难理解,因为计算的流程被隐藏在回调之间。如果你需要用回调来管理许多异步操作,你可能很快就会遇到回调地狱的问题。

虽然回调在JavaScript中有其良好的地位,但还是要找到一个更好的解决方案。

1.2 封装异步性

同步的代码很容易理解。你可以看到一行一行的代码是如何执行的。

如何编写异步操作的代码,同时仍然保留同步代码的可读性?

getList() ,返回一种人的列表呢?

然后,这个人的种类列表检查是否包含who然后将布尔值记录到控制台。最重要的是,这些种类的结果可以被返回,就像普通的对象一样分配给变量。

这种封装(又称持有、管理、包含)异步操作结果的结果对象是一个承诺对象。

承诺,包裹着异步操作的结果,可以从一个函数中同步返回,分配给变量,或作为参数使用。这就是承诺的想法:封装异步性,并允许处理异步操作的函数看起来仍然是同步的。

2.什么是承诺

一个承诺是一个封装了异步操作结果的对象。

每个承诺都有状态,它可以有以下值之一。

  • 待定
  • 以一个值完成
  • 因某种原因被拒绝

刚刚创建的承诺处于等待状态。只要后面的异步操作还在进行中,该承诺就会保持待定状态。

然后,根据异步操作的完成情况,承诺的状态会改变为

A)已完成(当异步操作成功完成时)

Promise fulfilled state

B)或拒绝(当异步操作失败时)。

Promise rejected state

在JavaScript中,你可以使用一个特殊的构造函数Promise 来创建一个承诺对象。

const promise = new Promise((resolve, reject) => {
  // Async operation logic here....
  if (asyncOperationSuccess) {
    resolve(value); // async operation successful
  } else {
    reject(error);  // async operation error
  }
});

Promise 构造函数接受一个特殊函数,该函数应包含异步操作的逻辑。

在该特殊函数中,在操作完成后:

  1. 如果异步操作成功完成,则调用resolve(value) - 将承诺的状态改为已完成,并以value
  2. 否则,在出现错误的情况下,调用reject(error) --将承诺的状态改为拒绝,理由是error

让我们从枯燥的理论中暂停一下,回到人们的例子中来。

就像我之前提到的,我希望函数getList() ,以返回一种--一个人的列表的承诺。

function getList() {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Joker', 'Batman']), 1000);
  });
}

getList() 创建并返回一个承诺。在承诺中,在通过1秒后,调用 ,有效地使承诺与人员列表一起resolve(['Joker', 'Batman']) 实现

虽然在后面的例子中,我是用手来创建承诺的,但在生产中通常不会这样做。大多数流行的库(如axios)或网络API(如fetch())的异步函数都返回已经构建好的承诺。

你可以使用一个特殊的方法访问一个承诺的履行值(简单地说,就是成功完成异步操作的结果)。

promise
  .then(value => {
    // use value...
  });

Promise then()

下面是如何访问由getList() 返回的承诺值。

function getList() {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Joker', 'Batman']), 1000);
  });
}
const promise = getList();
promise
  .then(value => {
    console.log(value); // logs ['Joker', 'Batman']
  });

在了解了如何从一个承诺中提取满足的值后,让我们转换一下findPerson(who) ,从getList() 返回的承诺中提取列表。

function getList() {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Joker', 'Batman']), 1000);
  });
}
function findPerson(who) {
  const listPromise = getList();
  listPromise
    .then(list => {
      const found = list.some(person => person === who);  
      console.log(found); // logs true
    });
}
findPerson('Joker');

仔细看看const listPromise = getList() 语句:你用一个同步语句得到了承诺,即使它后面运行着一个异步操作。

这是承诺的第一大好处(第二大好处是链式):你可以用同步的方式操作封装好的异步操作结果,而不像回调方法那样使函数过度复杂化。

如果操作未成功完成,promise会以错误的方式拒绝。你可以使用一个特殊的方法访问拒绝错误。

promise
  .catch(error => {
    // check error...
  })

Promise catch()

例如,让我们想象一下,访问人员列表的结果是一个错误(注意使用reject(error) 函数)。

function getList() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Nobody here!')), 1000);
  });
}
const promise = getList();
promise
  .catch(error => {
    console.log(error); // logs Error('Nobody here!')
  });

这一次promise 被拒绝,new Error('Nobody here!') 。你可以在提供给promise.catch(errorCallback) 的回调中访问该错误。

2.3 提取值和错误

你也可以一次性提取履行值和拒绝原因。你可以通过2种方式来做到这一点。

A) 向promise.then(successCallback, errorCallback) 方法提供两个回调。第一个回调successCallback ,当承诺被履行时被调用,而第二个回调errorCallback ,当拒绝时被调用。

promise
  .then(value => {
    // use value...
  }, error => {
    // check error...
  });

B)或者你可以使用所谓的承诺链(如下所述)和承诺链promise.then(successCallback).catch(errorCallback)

promise
  .then(value => {
    // use value...
  })
  .catch(error => {
    // check error...
  });

让我们仔细看看方法B),因为它用得更频繁。

当使用promise.then(successCallback).catch(errorCallback) 链时,如果promise 解决成功,那么只调用successCallback

function getList() {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Joker', 'Batman']), 1000);
  });
}
const promise = getList();
promise
  .then(value => {
    console.log(value); // logs ['Joker', 'Batman']
  })
  .catch(error) => {
    console.log(error); // Skipped...
  };

然而,如果promise 被拒绝,那么只有errorCallback 被调用。

function getList() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Nobody here!')), 1000);
  });
}
const promise = getList();
promise
  .then(value => {
    console.log(value); // Skipped...
  })
  .catch(error => {
    console.log(error); // logs Error('Nobody here!')
  });

3.承诺链

如上所述,一个承诺封装了一个异步操作的结果。你可以以任何方式使用承诺:从函数中返回,作为参数使用,分配给变量。这是第一个好处。

第二大好处是,承诺可以创建链来处理多个依赖性异步操作。

链的技术层面包括这样一个事实:promise.then(successCallback) ,甚至promise.catch(errorCallback) 方法本身就会返回一个承诺,你可以将.then().catch() 方法附加到这个承诺上,以此类推。

例如,让我们创建一个异步函数,将一个数字加倍,延迟1秒。

function delayDouble(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(2 * number), 1000);
  });
}

然后,让我们把这个数字翻3倍5

delayDouble(5)
  .then(value1 => {
    console.log(value1); // logs 10
    return delayDouble(value1);
  })
  .then(value2 => {
    console.log(value2); // logs 20
    return delayDouble(value2);
  })
  .then(value3 => {
    console.log(value3); // logs 40
  });

每个翻倍操作需要1秒。该链执行了3次翻倍操作,每次操作的结果都被下一次操作使用。

Chain of promises

在一个承诺链中,如果链中的任何承诺被拒绝,那么解析流就会跳到第一个.catch() ,绕过中间的所有.then()

delayDouble(5)
  .then(value1 => {
    console.log(value1); // logs 10
    return new Promise((_, reject) => reject(new Error('Oops!')));
  })
  .then(value2 => {
    console.log(value2); // Skipped...
    return delayDouble(value2);
  })
  .then(value3 => {
    console.log(value3); // Skipped...
  })
  .catch(error => {
    console.log(error); // logs Error('Oops!')
  });

4.async/await

在看了前面使用承诺的代码样本后,你可能会想。

使用承诺仍然需要回调和相对大量的模板代码,如.then(),.catch()

你的观察是合理的。

幸运的是,JavaScript通过提供async/await 语法,在改善异步代码方面又向前迈进了一步--这是在promises之上的一个真正有用的语法糖。

在可能的情况下,我强烈建议使用async/await 语法,而不是处理原始承诺。

在承诺之上应用async/await 语法是相对容易的。

  • async 关键字标记使用承诺的函数
  • async 函数主体内,只要你想等待一个承诺的解决,就使用await promiseExpression 语法
  • 一个async 函数总是返回一个承诺,这样就可以在async 函数中调用async 函数。

4.1等待承诺值

如果承诺被履行,await promise 语句会评估为履行值。

async function myFunction() {
  // ...
  const value = await promise;
}

JavaScript async await of promise

当JavaScript遇到await promise ,其中promise 是待定的,它将暂停函数的执行,直到promise 得到履行或拒绝。

现在让我们使用async/await 语法来访问延迟的列表。

function getList() {
  return new Promise(resolve => {
    setTimeout(() => resolve(['Joker', 'Batman']), 1000);
  });
}
async function findPerson(who) {
  const list = await getList();
  const found = list.some(person => person === who);
  console.log(found); // logs true
}
findPerson('Joker');

在承诺实现后,表达式async findPerson(who) ,评估为实际的人员列表。

看一下async findPerson(who) 这个函数,你会发现它与本帖开头的那个函数的同步版本是多么的相似!这就是承诺的目的。这就是承诺和async/await 语法的目的。

4.2捕捉承诺错误

如果承诺在被等待时被拒绝,你可以通过将await promise 包裹到try/catch 子句中来轻松捕获错误。

async function myFunction() {
  // ...
  try {
    const value = await promise;
  } catch (error) {
    // check error
    error;
  }
}

JavaScript async catch

例如,让我们拒绝应该返回人员列表的承诺。

function getList() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Nobody here!')), 1000);
  });
}
async function findPerson(who) {
  try {
    const list = await getList();
    const found = list.some(person => person === who);
    console.log(found); 
  } catch (error) {
    console.log(error); // logs Error('Nobody here!')
  }
}
findPerson('Joker');

这一次,承诺await getList() 拒绝。执行立即跳转到catch(error) :其中error 表示拒绝的原因--new Error('Nobody here!')

4.3等待链(await-ing chain

你可以在一个async 函数内使用任意多的await 语句。例如,让我们把承诺链部分的例子转换成async/await 语法。

function delayDouble(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(2 * number), 1000);
  });
}
async function run() {
  const value1 = await delayDouble(5);
  console.log(value1); // logs 10
  
  const value2 = await delayDouble(value1);
  console.log(value2); // logs 20
  const value3 = await delayDouble(value2);
  console.log(value3); // logs 40
}
run();

显然,async/await 大大简化了对多个依赖性异步操作的处理。

5.总结

promise是一个占位符,持有一个异步操作的结果。如果操作成功完成,那么诺言就会以操作值来实现,但如果操作失败:诺言就会以失败的原因来拒绝

许诺还可以创建链,这在处理多个依赖性异步操作时非常有用。