几年前,我在学习诺言时曾遇到过理解上的困难。
问题是大多数的教程都只是在描述承诺对象,它的方法等等。但我并不太关心诺言,我关心的是,只要它能让编码变得更简单
下面是我想读的帖子,以便自己理解诺言。这篇帖子描述了为什么承诺能使异步逻辑的编码更容易,然后解释了如何正确使用承诺,包括用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)已完成(当异步操作成功完成时)
B)或拒绝(当异步操作失败时)。
在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 构造函数接受一个特殊函数,该函数应包含异步操作的逻辑。
在该特殊函数中,在操作完成后:
- 如果异步操作成功完成,则调用
resolve(value)- 将承诺的状态改为已完成,并以value - 否则,在出现错误的情况下,调用
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...
});
下面是如何访问由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...
})
例如,让我们想象一下,访问人员列表的结果是一个错误(注意使用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次翻倍操作,每次操作的结果都被下一次操作使用。
在一个承诺链中,如果链中的任何承诺被拒绝,那么解析流就会跳到第一个.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遇到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;
}
}
例如,让我们拒绝应该返回人员列表的承诺。
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是一个占位符,持有一个异步操作的结果。如果操作成功完成,那么诺言就会以操作值来实现,但如果操作失败:诺言就会以失败的原因来拒绝。
许诺还可以创建链,这在处理多个依赖性异步操作时非常有用。