原文链接:http://javascript.info/promise-basics,translate with ❤️ by zhangbao.
想象一下,你是一名顶尖歌手,粉丝们会日夜问你即将到来的单曲。
为了得到一些缓解,你保证在发布时将它发送给他们。您可以为粉丝提供可以订阅的更新列表,在填写他们电子邮件地址后,就可以在歌曲发布时,收到通知;即使出现问题,例如,如果发布歌曲的计划被取消,仍会收到通知。
每个人都很开心,因为人们不再拥挤你和粉丝也得到满足——因为不会错过单曲。
这是我们在编程中经常遇到的一个类比:
“生产代码”,做一些花费时间的事情。例如,加载一个远程脚本。对比到上例中的“歌手”。
“消费代码”,依赖“生成代码”产生的结果。许多功能的实现可能需要这个结果。对应上例中的“粉丝”。
Promise 是一个特殊的 JavaScript 对象,它将“生成代码”和“消费代码”链接在一起。 就我们的比喻而言:这就是“订阅列表”。“生成代码”需要花费一些时间来产生 Promise 的结果,一旦结果准备好,所有的订阅代码都能获得执行。
这个类比并不十分准确,因为 JavaScript 的 Promise 比的订阅列表更复杂:它们有附加的特性和限制。但一开始从它展开理解会很好。
Promise 构造函数语法是:
let promise = new Promise(function(resolve, reject) {
// 执行器 (生产代码, "歌手")
});
传递给 new Promise()
构造函数的回调称为执行器。创建 Promise 的时候,执行器函数会自动执行,它包含生产代码,最终产生一个结果。类比到上例中:执行器就是“歌手”。
生成的 promise
对象具有内部属性:
state
——初始状态是“pending”,之后改变为“fulfilled”或“rejected”,result
——你选择的任意值,初始值为undefined
。
当执行器执行完毕后,它应该调用传递进去的两个参数(函数类型的)之一:。
resolve(value)
——表示任务成功完成:将
state
值设置为“fulfilled
”,将
result
值设置为value
。
reject(error)
——表示错误发生:将
state
值设置为“rejected
”,将
result
值设置为error
。
稍后我们将看到这些变化是如何通知到“粉丝”的。
下面是一个 Promise
构造函数和一个具有简单“生成代码”的执行器例子(使用了 setTimeout
):
let promise = new Promise(function(resolve, reject) {
// 调用 Promise 构造函数的时候,这个函数会自动执行
// 一秒后使用结果 "done!" 结束任务
setTimeout(() => resolve("done!"), 1000);
});
运行上面的代码,我们可以看到:
执行器会立即自动调用(通过
new Promise
)。执行器接受两个参数:
resolve
和reject
——这些函数是由 JavaScript 引擎预定义的。因此我们无需创建它们。相反,在执行器准备好时,就能直接调用它们了。
经过 1 秒钟的“处理”之后,执行器调用 resolve('done')
产生结果:
这是一个成功结束的例子,得到了一个“fulfilled 状态的 Promise”。
现在,在举一个使用错误 reject Promise 的例子:
let promise = new Promise(function(resolve, reject) {
// 1秒钟后任务携带错误结束
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
总而言之,执行器里会先做一件事情(通常需要花费点时间),之后调用 resolve
或 reject
来改变 Promise
对象状态。
对应“pending”状态的 Promise,resolved 或 rejected 状态的 Promise 称为是“解决”了的。
结果只有一个:成功或者失败
在执行器中,仅能调用
resolve
或者reject
,Promise 状态一旦改变,就无法再更改。再有调用
resolve
/reject
的地方都会被忽略:```javascript let promise = new Promise(function(resolve, reject) { resolve(“done”);
reject(new Error(“…”)); // 忽略 setTimeout(() => resolve(“…”)); // 忽略 });
>
> 其思想是,执行器只能产生一种结果:正常返回或发生错误。
>
> 而且,`resolve`/`reject` 只接受一个参数的调用,其余参数都会被忽略。
> ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**使用 Error 对象 reject**
>
> 一旦发生错误,我们可以使用任何类型参数调用 `reject`(就像 `resolve`)。但是建议使用 `Error` 对象(或者继承自 `Error` 的其他类型),这样做的理由很快就会显现出来。
> ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**立即调用 `resolve`/`reject`**
>
> 实践中,执行器中通常会做一些异步操作,经过一段时间之后才调用 `resolve`/`reject`,但这不是必须的。我们也可以立即调用 `resolve`/`reject`:
>
> ```javascript
let promise = new Promise(function(resolve, reject) {
// 没有费时操作,直接结果任务
resolve(123); // 立即给出结果: 123
});
例如,当我们开始做一些操作时,发现一切都已完成时,就对应这种情况。
这很好。我们马上就得到了一个 resolved 状态的 Promise,并且没有错。
state
和result
都是内部属性Promise 对象上的
state
和result
属性都是内部属性。我们不能直接在“消费代码”里直接访问。我们可以使在.then
/.catch
方法中使用它们产生的结果。它们会在下面介绍。
消费者:“then”和“catch”
Promise 对象作为执行器(“生产代码”或叫“歌手”)和消费代码(“粉丝”)之间的连接,用来接收结果或错误。消费函数是通过 .then
和 .catch
方法注册(订阅)的。
.then
的语法是这样的:
promise.then(
function(result) { /* 处理成功结果 */ },
function(error) { /* 处理错误 */ }
);
.then
的第一个参数是一个函数:
在 Promise 变为 resolved 状态时执行,并且
接受处理结果作为参数。
第二个参数也是一个函数:
在 Promise 变为 rejected 状态时执行,并且
接受错误作为参数。
下面是处理成功返回的 Promise 的例子:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// Promise 变为 resolved 状态,执行 .then 的第一个(函数类型)参数
promise.then(
result => alert(result), // 1秒后,显示 "done!"
error => alert(error) // 不会执行
);
第一个函数被执行。
如果 Promise reject 了,则执行第二个:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// Promise 变为 rejectd 状态,执行 .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!"
如果我们只关闭错误情况,那么可以使用 null
作为 .then
方法的第一个参数:.then(null, errorHandlingFunction)
,这与使用 .catch(errorHandlingFunction)
是一样的:
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) 等同于 promise.then(null, f)
promise.catch(alert); // 1秒后,显示 "Error: Whoops!"
.catch
完全可以看作是 .then(null, f)
的一种简写形式。
对于已解决 Promise,
.then
方法会立即执行如果 Promise 处于 pending 状态,那么
.then
/catch
方法会一直等待结果出来才执行。如果 Promise 是已解决状态的话,就会立即执行.then
/catch
:```javascript // resolved 状态 Promise,会立即执行 .then let promise = new Promise(resolve => resolve(“done!”));
promise.then(alert); // done! (立即显示)
>
> 有些任务的完成,有时需要些时间,有时则会立即完成。无论哪种情形,好处是:`.then` 处理程序都能保证正确的运行。
> ![](https://cdn.nlark.com/yuque/0/2018/png/103346/1544062755364-fcf09fa7-0c35-481e-8116-c0679055a0c9.png#width=27)**`.then`/`catch` 处理器总是异步执行的**
>
> 即使 Promise 立即得到解决,`.then`/`catch` 方法下面的代码也会先执行。
>
> JavaScript 引擎内部维护了一个执行队列,收集所有的 `.then`/`catch` 处理器。
>
> 但是,只有在当前执行队列完成时,它才会查看这个队列。
>
> 也就是说,`.then`/`catch` 处理器直到引擎执行完当前代码后,才开始执行。
>
> 例如,这里:
>
> ```javascript
// "立即" resolved 的 Promise
const executor = resolve => resolve("done!");
const promise = new Promise(executor);
promise.then(alert); // 这个 alert 是后展示 (*)
alert("code finished"); // 这个 alert 先展示
上面的 Promise 立即得到解决,但是引擎首先完成的是当前代码,调用
alert
;然后才查看.then
处理器队列,去运行。因此,
.then
之后的代码总是在 Promise 订阅者之前执行。即使是对于一个立即得到解决的 Promise。这通常并不重要,但在某些情况下,是在意顺序的。
接下来,我们要看更多实际的例子,解释 Promise 如何助力我们编写异步代码的。
例子:loadScript
在上一章里,我们定义了一个 loadScript
函数。
这种写法是基于回调的,在这里为了提醒我们写过的东西:
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 ` + src));
document.head.append(script);
}
让我们用 Promise 来重写它。
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: " + src));
document.head.append(script);
});
}
使用:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('One more handler to do something else!'));
我们可以立即看到基于回调的模式的一些好处:
短处 | 长处 |
---|---|
- 当调用 loadScript 的时候,必须提供一个 callback 函数。也就是说,我们在调用 loadScript 的之前就要知道要对结果怎样处理。 |
- 只能提供一个回调函数。
|
- Promise 允许我们按照自然规律做事。首先,我们执行 loadScript
,然后我们在 .then
里写对结果执行的操作。
- 我们可以在一个 Promise 上多次调用 .then
方法。每一次,我们添加一个新的“粉丝”。一个新的订阅函数,到“订阅列表”。下一节将详细介绍:Promise 链式调用。
|
因此,Promise 已经为我们提供更好了的代码流和灵活性。下一章中,我们会看到更多的 Promise 相关知识。
练习题
问题
一、可以重复 resolve 一个 Promise?
下面代码的输出结果是什么?
let promise = new Promise(function(resolve, reject) {
resolve(1);
setTimeout(() => resolve(2), 1000);
});
promise.then(alert);
二、延迟 Promise
内置函数 setTimeout
使用回调。创建一个可选的基于 Promise 的形式。
函数 delay(ms)
返回一个 Promise。Promise 会在 ms
毫秒之后 resolve,因此我们可以在之后加 .then
来处理。像这样:
function delay(ms) {
// 你的代码
}
delay(3000).then(() => alert('3秒后运行'));
三、使用 Promise 实现动圆功能
重写一下 Animated circle with callback 任务的 showCircle
函数实现,改成返回 Promise 而非使用回调的形式。
新的使用方式:
showCircle(150, 150, 100).then(div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
作为基础,你可以先看下 Animated circle with callback 任务的实现效果。
答案
一、可以重复 resolve 一个 Promise?
输出是 1
。
第二个 resolve
调用会被忽略,因为只有第一次的 reject
/resolve
调用是有效的。其他再多一次的调用都会被忽略。
二、延迟 Promise?
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(3000).then(() => alert('3秒后运行'));
需要注意的是,这里的 resolve
函数在调用时,没有提供参数。delay
没有返回任何值,只是为了延迟时间。
三、使用 Promise 实现动圆功能
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.message-ball {
font-size: 20px;
line-height: 200px;
text-align: center;
}
.circle {
transition-property: width, height, margin-left, margin-top;
transition-duration: 2s;
position: fixed;
transform: translateX(-50%) translateY(-50%);
background-color: red;
border-radius: 50%;
}
</style>
</head>
<body>
<button onclick="go()">Click me</button>
<script>
function go() {
showCircle(150, 150, 100).then(div => {
div.classList.add('message-ball');
div.append("Hello, world!");
});
}
function showCircle(cx, cy, radius) {
let div = document.createElement('div');
div.style.width = 0;
div.style.height = 0;
div.style.left = cx + 'px';
div.style.top = cy + 'px';
div.className = 'circle';
document.body.append(div);
return new Promise(resolve => {
setTimeout(() => {
div.style.width = radius * 2 + 'px';
div.style.height = radius * 2 + 'px';
div.addEventListener('transitionend', function handler() {
div.removeEventListener('transitionend', handler);
resolve(div);
});
}, 0);
})
}
</script>
</body>
</html>
在线查看。
(完)