JavaScript Promises 和异步编程简介
承诺
方法
Async 和 Await
结论
立即在http://jauyeung.net/subscribe/订阅我的电子邮件列表
在 Twitter 上关注我:https://twitter.com/AuMayeung
更多文章请访问https://medium.com/@hohanga
由于 JavaScript 是单线程语言,同步代码一次只能执行一行。这意味着,如果同步代码的运行时间超过瞬时,它将停止其余代码的运行,直到正在运行的代码完成为止。为了防止运行时间不确定的代码阻碍其他代码的运行,我们需要使用异步代码。
承诺
要在 JavaScript 中实现这一点,我们可以在代码中使用 Promise。Promise 是表示运行时间不确定的进程的对象,它可能导致成功或失败。要在 JavaScript 中创建 Promise,我们使用一个Promise
用于创建 Promise 的构造函数对象。Promise
构造函数接受一个名为 executor 的函数,该函数具有resolve
和reject
参数。这两个参数都是函数,我们调用它们来履行 Promise(这意味着以某个值成功结束它)或拒绝 Promise(这意味着返回某个错误值并将 Promise 标记为失败)。函数的返回值将被忽略。因此,Promise 不能返回除 Promise 之外的任何值。
例如,我们可以在 JavaScript 中定义一个承诺,如以下代码所示:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('abc'), 1000);
});
上面的代码创建了一个 Promise,它会abc
在一秒后使用值来实现该 Promise。因为我们setTimeout
在执行器函数内部运行,以便abc
在一秒内使用值来解决该 Promise,所以这是异步代码。我们无法abc
在的回调函数中返回值setTimeout
,所以我们必须调用resolve('abc')
来获取已解析的值。我们可以使用then
函数访问已实现 Promise 的解析值。该then
函数接受一个回调函数,该回调函数将已实现 Promise 的解析值作为参数。您可以在那里获取该值并对其执行所需的操作。例如,我们可以执行以下操作:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('abc'), 1000);
});
promise.then((val) => {
console.log(val);
})
当我们运行上面的代码时,我们应该会看到abc
日志。正如我们所见,当 Promise 通过调用函数实现时,Promise 将会提供相应的值resolve
。
一个 Promise 有三种状态。它可以是 pending,表示该 Promise 已被 fulfilled 或 rejected。它可以是 fulfilled,表示操作已成功完成。也可以是 rejection,表示该 Promise 操作失败。
待处理的 Promise 可以以某个值完成,也可以以某个错误被拒绝。当 Promise 完成时,函数会获取相应的解析值then
,并调用传入函数的回调函数then
。如果 Promise 被拒绝,我们可以选择使用函数捕获错误catch
,该函数也接受一个带有错误信息的回调函数。then
和catch
函数都返回 Promise,因此它们可以链接在一起。
例如,我们可以写如下内容:
const promise = (num) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve('resolved')
} else {
reject('rejected')
}
}, 1000);
});
}
promise(1)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})promise(2)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
在上面的代码中,我们有一个函数,它返回一个 JavaScript Promise,当值为 1 时,promise
它会执行 Promise 的值,否则会抛出错误并拒绝 Promise 。因此我们运行:resolved
num
rejected
promise(1)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
然后then
函数运行,由于值为 1,函数调用返回的 Promisepromise(1)
被满足,num
并且解析后的值可以在 中设置val
。因此,运行时console.log(val)
会resolved
记录日志。运行以下代码:
promise(2)
.then((val) => {
console.log(val);
})
.catch((error) => {
console.log(error);
})
然后catch
函数运行,因为函数调用返回的 Promisepromise(2)
被拒绝了,而这个被拒绝的错误值可以在 error 中设置。所以运行时console.log(error)
会rejected
打印日志。
JavaScript 的 Promise 对象具有以下属性:length
和prototype
。length
的值为 1,表示构造函数参数的数量,即始终为 1。prototype
属性表示 Promise 对象的原型。
Promise 还具有一个finally
方法,用于运行无论 Promise 是 fulfilled 还是 rejected 都会运行的代码。该finally
方法接受一个回调函数作为参数,无论 Promise 的结果如何,任何你想要运行的代码都会在这个回调函数中运行。例如,如果我们运行:
Promise.reject('error')
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log('finally runs');
})
然后我们获取error
并finally runs
记录,因为原始承诺因原因而被拒绝error
。然后该方法回调中的任何代码finally
都会运行。
使用 Promise 编写异步代码的主要好处是我们可以使用它们按顺序运行异步代码。为此,我们可以将 Promise 与then
函数链接起来。该then
函数接受一个在 Promise 实现时运行的回调函数。当 Promise 被拒绝时,它还接受第二个参数。要链接 Promise,我们必须返回另一个 Promise 作为函数第一个回调函数的返回值then
。如果我们不想将另一个 Promise 链接到现有的 Promise,它可以返回其他值,比如什么也不返回。我们可以返回一个值,该值将被解析并且可以在下一个then
函数中检索到。它也可以抛出一个错误。然后,返回的 Promisethen
将被拒绝,并将抛出的错误作为值。它还可以返回一个已经实现或被拒绝的 Promise,当一个函数链接在它之后时,它将获得实现的值then
,或者在函数的回调中获取错误原因catch
。
例如,我们可以写以下内容:
Promise.resolve(1)
.then(val => {
console.log(val);
return Promise.resolve(2)
})
.then(val => {
console.log(val);
})Promise.resolve(1)
.then(val => {
console.log(val);
return Promise.reject('error')
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));Promise.resolve(1)
.then(val => {
console.log(val);
throw new Error('error');
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));
在第一个例子中,我们将 Promise 串联起来,它们都解析为一个值。所有 Promise 都已解析为值。在第二个和最后一个例子中,我们拒绝第二个 Promise 或抛出一个错误。它们的作用相同。第二个 Promise 被拒绝,错误原因将记录在catch
函数的回调中。我们也可以串联待处理的 Promise,如下例所示:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
promise1
.then(val => {
console.log(val);
return promise2;
})
.then(val => {
console.log(val);
})
.catch(error => console.log(error));
第一个函数的回调then
返回promise2
,这是一个待处理的承诺。
方法
JavaScript 承诺具有以下方法。
Promise.all(可迭代)
Promise.all
接受一个可迭代对象,该对象允许我们在某些计算机上并行运行多个 Promise,而在其他计算机上串行运行。这对于运行多个互不依赖的 Promise 非常方便。它接受一个包含 Promise 列表(通常为数组)的可迭代对象,然后返回一个 Promise,Promise
当迭代对象中的所有 Promise 都解析后,该 Promise 也解析成功。
例如,我们可以编写如下代码来运行多个承诺Promise.all
:
const promise1 = Promise.resolve(1);
const promise2 = 2;
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values);
});
如果我们运行上面的代码,应该console.log
会打印日志[1,2,3]
。如我们所见,它仅在所有 Promise 都已完成后才返回已解决的值。如果其中一些 Promise 被拒绝,那么我们将不会获得任何已解决的值。相反,我们将获得被拒绝的 Promise 返回的任何错误值。它会在第一个被拒绝的 Promise 处停止,并将该值发送给catch
函数的回调函数。例如,如果我们有:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject(3), 1000);
});
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
.catch(error => {
console.log(error);
});
console.log
然后我们从函数的回调中得到记录的 2 catch
。
Promise.allSettled
Promise.allSettled
返回一个 Promise,该 Promise 在所有给定的 Promise 都解析或拒绝后解析。它接受一个可迭代对象,其中包含一组 Promise,例如一个 Promise 数组。返回的 Promise 的解析值是一个包含每个 Promise 最终状态的数组。例如,假设我们有:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject(3), 1000);
});
Promise.allSettled([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
如果我们运行上面的代码,我们会得到一个包含三个条目的数组,每个条目都是一个对象,它包含已完成的 Promise 的status
和属性,以及一个包含已拒绝的 Promise 的和属性。例如,上面的代码将记录、、。成功 Promise 的状态会被记录,拒绝 Promise 的状态也会被记录。value
status
reason
{status: “fulfilled”, value: 1}
{status: “rejected”, reason: 2}
{status: “rejected”, reason: 3}
fulfilled
rejected
Promise.race
该Promise.race
方法返回一个 Promise,该 Promise 解析为第一个已完成的 Promise 的已解析值。它接受一个可迭代对象,其中包含一组 Promise,例如一个 Promise 数组。如果传入的可迭代对象为空,则返回的 Promise 将永远处于待处理状态。如果可迭代对象包含一个或多个非 Promise 值,或者包含已完成的 Promise,则将Promise.race
返回其中第一个。例如,如果我们有:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
然后我们看到它1
被打印出来了。这是因为promise1
它是第一个被解析的,因为它在下一行运行之前就被立即解析了。同样,如果我们在数组中有一个非 Promise 值作为参数传入,就像下面的代码一样:
const promise1 = 1;
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve(3), 1000);
});
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
然后,我们得到相同的输出,因为它是我们传递给方法的数组中的非 Promise 值Promise.race
。同步代码始终在异步代码之前运行,因此同步代码的位置无关紧要。如果我们有:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
const promise3 = 3;
Promise.race([promise1, promise2, promise3])
.then((values) => {
console.log(values);
})
然后我们3
进行记录,因为setTimeout
将回调函数放入队列中以便稍后运行,所以它将比同步代码更晚运行。
最后,如果我们有:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
Promise.race([promise1, promise2])
.then((values) => {
console.log(values);
})
然后我们2
进入控制台日志,因为一秒钟内解决的承诺将比两秒钟内解决的承诺更早得到解决。
Promise.reject
Promise.reject
返回一个因原因而被拒绝的 Promise。使用 的实例对象来拒绝 Promise 很有用Error
。例如,如果我们有以下代码:
Promise.reject(new Error('rejected'))
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
然后我们就rejected
登录了。
Promise.resolve
Promise.resolve
返回一个 Promise,该 Promise 解析为传递给函数参数的值resolve
。我们还可以传入一个具有then
属性的对象,其值是 Promise 的回调函数。如果该值包含then
方法,则该 Promise 将使用该函数实现的值来实现then
。也就是说,函数的第一个参数与函数的值then
相同resolve
,第二个参数与函数的值相同。reject.
例如,我们可以编写以下内容:
Promise.resolve(1)
.then((value) => {
console.log(value);
})
然后我们得到1
记录,因为1
这是我们传递给函数的值resolve
,以返回具有已解析值的承诺1
。
如果我们传入一个带有then
方法的对象,如下面的代码所示:
Promise.resolve({
then(resolve, reject) {
resolve(1);
}
})
.then((value) => {
console.log(value);
})
然后我们得到1
记录的值。这是因为该Promise.resolve
函数将运行该then
函数,并且resolve
设置为then
属性的函数参数将被假定为resolve
在 Promise 中调用该函数的函数。如果我们在传入的对象中调用reject
该函数内部的函数then
,那么我们会得到一个被拒绝的 Promise,如下例所示:
Promise.resolve({
then(resolve, reject) {
reject('error');
}
})
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
在上面的代码中,error
由于承诺被拒绝,我们进行了记录。
Async 和 Await
使用async
和await
,我们可以缩短 Promise 代码。在使用async
和之前await
,我们必须使用then
函数,并且必须将回调函数作为所有then
函数的参数。由于 Promise 很多,这会使代码变得很长。相反,我们可以使用async
和await
语法来替换then
函数及其关联的回调。例如,我们可以将以下代码从:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
promise1
.then((val1) => {
console.log(val1);
return promise2;
})
.then((val2) => {
console.log(val2);
})
到:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000);
});
(async () => {
const val1 = await promise1;
console.log(val1)
const val2 = await promise2;
console.log(val2)
})()
我们将then
和 回调替换为await
。然后,我们可以将每个 Promise 的解析值赋值为变量。请注意,如果我们使用 来await
作为 Promise 代码,则必须async
像上例一样添加函数签名。为了捕获错误,catch
我们使用 子句,而不是将函数链接在末尾catch
。此外,我们不是finally
在底部链接函数以在 Promise 结束时运行代码,而是在 子句finally
后使用 子句catch
。
例如,我们可以写:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject('error'), 1000);
});
(async () => {
try {
const val1 = await promise1;
console.log(val1)
const val2 = await promise2;
console.log(val2)
} catch (error) {
console.log(error)
} finally {
console.log('finally runs');
}})()
在上面的代码中,我们将 Promise 的解析值赋给了一个变量,而不是像上一行then
那样在函数的回调中获取值const response = await promise1
。此外,我们使用了try...catch...finally
代码块来捕获被拒绝的 Promise 的错误,并使用了finally
子句而不是finally
传入回调的函数来创建无论 Promise 发生什么都会运行的代码。
与其他使用 Promise 的函数一样,async
函数始终返回 Promise,不能返回任何其他内容。在上面的示例中,我们展示了如何以比将then
回调作为参数传入的函数更简洁的方式链接 Promise。
结论
有了 Promise,我们可以轻松编写异步代码。Promise 是表示一个运行时间不确定的进程的对象,该进程可能成功也可能失败。要在 JavaScript 中创建 Promise,我们使用一个Promise
构造函数对象。
构造Promise
函数接受一个名为 executor 的函数,该函数包含resolve
和reject
参数。这两个参数都是函数,我们调用它们来要么实现 Promise(这意味着成功结束并返回某个值),要么拒绝 Promise(这意味着返回某个错误值并将 Promise 标记为失败)。该函数的返回值会被忽略。因此,Promise 不能返回除 Promise 之外的任何值。
Promise 是可链式执行的,因为 Promise 会返回 Promise。Promisethen
的函数可以抛出错误、返回已解决的值,或者返回其他处于待处理、已完成或已拒绝状态的 Promise。