原文链接: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` 只接受一个参数的调用,其余参数都会被忽略。> **使用 Error 对象 reject**>> 一旦发生错误,我们可以使用任何类型参数调用 `reject`(就像 `resolve`)。但是建议使用 `Error` 对象(或者继承自 `Error` 的其他类型),这样做的理由很快就会显现出来。> **立即调用 `resolve`/`reject`**>> 实践中,执行器中通常会做一些异步操作,经过一段时间之后才调用 `resolve`/`reject`,但这不是必须的。我们也可以立即调用 `resolve`/`reject`:>> ```javascriptlet 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` 处理程序都能保证正确的运行。> **`.then`/`catch` 处理器总是异步执行的**>> 即使 Promise 立即得到解决,`.then`/`catch` 方法下面的代码也会先执行。>> JavaScript 引擎内部维护了一个执行队列,收集所有的 `.then`/`catch` 处理器。>> 但是,只有在当前执行队列完成时,它才会查看这个队列。>> 也就是说,`.then`/`catch` 处理器直到引擎执行完当前代码后,才开始执行。>> 例如,这里:>> ```javascript// "立即" resolved 的 Promiseconst 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>
在线查看。
(完)
结果只有一个:成功或者失败