Why Promise?
Promise 的出现是为了解决异步编程中,主要使用的回调机制的几个问题:
- Callback hell:Promise 可以把一层层嵌套的 callback 变成
.then().then()...
,从而使代码编写和阅读更直观 - 错误处理难:Promise 比 callback 在错误处理上更清晰直观
- 同时进行多个异步操作的代码编写困难:Promise 可以简单处理此情况
Google 的这篇 文档 写得非常好。但是它引入 Promise 用的 img.ready()
让我觉得没有道理,毕竟 Promise 函数更像是你告诉它开始干活(所以应该是 img.display()
)而不应该像事件一样的 img.ready()
。
目前看浏览器环境中,使用 Promise 的场景主要就是发异步请求。
Promise 的重点在于:
- 它是如何解决 callback hell,并提供直观的 API 和机制去让异步代码编写变得简单
- 在此基础上,它怎样让控制流变得清晰(比如多个 promise 链在一起时,发生错误时应该怎样流转)
- 有哪些常见的问题场景以及相应的解决方式(即是 pattern)
What's a Promise
一个 Promise 对象中定义的主要是一段执行具体操作的代码,并且在这段代码中,会执行两个回调函数,一个表示操作成功(resolve
),一个表示操作失败(reject
),比如下面这段发起 AJAX 请求的代码(关注 new Promise()
开始那部分):
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
Promise 构造函数中的参数被称为 executor。我们在后面还会提到它。
调用 get(url)
函数会生成一个 Promise 实例。当调用这个实例的 .then()
方法时,Promise 中定义的代码会被开始被执行。一个 Promise 实例有这几个状态:
- pending:未确定状态。刚
new
出来的 Promise 处于这个状态;然后会马上执行 executor 中定义的语句 - resolved:代码执行到
resolve()
语句后 - rejected:代码执行到
reject()
语句后 - settled:resolved 或者 rejected 状态都属于 settled
实例调用的代码如下:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
.then()
可以接受两个参数,第一个是执行成功后的回调函数,第二个是失败后的回调函数。executor 中调用 resolve()
或者 rejected()
时传的参数,会被这两个回调函数获得。
如何解决 callback hell?
.then()
看起来并没有绕开 callback 的模式。那 Promise 是如何解决 callback hell 的?答案就在 Promise 的 chaining。
.then()
的参数——即这两个回调函数,可以是多种类型的:
- 没有返回值的函数,会使得 Promise 链不再延续,此时你再往后面调用
.then()
是没有作用的。比如:Promise.resolve('foo').then(function(s) { console.log(s); }).then(function(s) { // Never executed console.log(s); }); // Output: // foo
- 有返回值的函数,会使 Promise 链可以继续,如:
Promise.resolve('foo').then(function(s) { console.log(s); return s + 'bar'; }).then(function(s) { console.log(s); }); // Output: // foo // foobar
- 有返回值且返回值为另一个 Promise 对象的函数,也会使 Promise 继续。与前者的区别在于,再次调用
.then()
时可能会触发的是异步操作,因此不是马上触发下一轮resolve()
:Promise.resolve('foo').then(function(s) { return new Promise((resolve, reject) => { console.log(s); setTimeout(() => { resolve(s + 'bar') }, 1000); }); }).then(function(s) { console.log(s); }); // Output: // foo // foobar (1s after displaying "foo")
A Realworld Example
// Including the `get()` function above...
function getJSON(url) {
return get(url).then(JSON.parse);
}
// Get chapter list, then get content of chapter 1
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Control Flow (and error handling)
当调用 .then()
时同时提供了两个参数,那么只会有其中一个被调用:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
如果是 .then(resolve_func).catch(reject_func)
的形式:
get('story.json').then(function(response) {
a + 1 // throws ReferenceError
}).catch(function(error) {
console.log("Failed!", error);
})
那么在 get('story.json')
以及 catch
之前的 resolve 函数中的 a + 1
报错都会被捕捉。
Promise 定义的规则是,错误只会往后抛,直到遇到下一个 .catch
或者带 reject 函数的 .then
。Google 的文档中提供了一个很好的例子:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
执行流为:
Promise in Practice
Promise 代码编写起来,相对同步代码有两个挑战。
数据传递
由于 Promise 代码的异步特性,以及大量使用函数,因此变量数据的传递和共享变得复杂。
设想有这样的同步代码:
let story = getJsonSync('story.json');
addHtmlToPage(story.heading);
for (let chapterUrl of story.chapterUrls) {
let chapter = getJsonSync(chapterUrl);
addHtmlToPage(chapter.html);
}
如果要写成异步,就会比较麻烦。因为你需要把各个 Promise 串起来,这其中数据的作用域只在各自的 resolve 函数中。你可以通过几种方式共享数据:
- 嵌套 Promise,即是在一个 resolve 函数中再去新建 Promise 并调用
.then()
:getJson('story.json').then(story => { addHtmlToPage(story.heading); return story.chapterUrls.reduce((sequence, chapterUrl) => { // 注意这里应该是 sequence.then() 打头。不然整条 Promise 链不起来。实际模拟一把 reduce 过程可以理解 return sequence.then(() => { return getJson(chapterUrl); }).then(chapter => { addHtmlToPage(chapter.html); }) }, Promise.resolve()); })
- 将数据通过
resolve(data)
或者在 executor 中返回数据来传递给下一环:getJson('story.json').then(story => { addHtmlToPage(story.heading); return story.chapterUrls; }).then(chapterUrls => { chapterUrls.reduce((sequence, chapterUrl) => { return sequence.then(() => { return getJson(chapterUrl); }).then(chapter => { addHtmlToPage(chapter.html); }) }, Promise.resolve()); });
- 在 Promise 代码外设置变量用于传递数据:
// 这个例子比较啰嗦,仅供说明做法 let chapterContents = []; getJson('story.json').then(story => { addHtmlToPage(story.heading); return story.chapterUrls.reduce((sequence, chapterUrl) => { return sequence.then(() => { return getJson(chapterUrl); }).then(chapter => { chapterContents.push(chapter.html) }) }, Promise.resolve()); }).then(() => { for (let chapterContent of chapterContents) { addHtmlToPage(chapterContent); } })
同步执行多事件的完成时间的不一致性
Promise 代码中,特别值得注意的一点是,Promise 的 executor 在 Promise 对象被创建时就被执行了。因此如果想实现同时做多任务,但是对任务结果的处理是按顺序来的,需要一定的技巧。
下面演示如何同时下载多个章节内容,但是按顺序将最快获取到的章节显示出来。效果是:
拉取到章节的顺序 | 2 | 1 | 3 | 5 | 4 |
---|---|---|---|---|---|
显示内容 | 不显示 | 显示 1, 2 | 添加 3 | 不添加 | 添加 4, 5 |
代码如下:
function parallel_download_and_display_immediately() {
getJson('story.json').then(story => {
addHtmlToPage(story.heading);
// 1. 创建拉取各章节的 Promise 对象,使得实际拉取动作开始
// 2. 使用 Array.reduce pattern 把各 Promise 对象的 `.then()` 串起来,
// 实现章节拉取完成的结果是被顺序处理的
return story.chapterUrls.map(getJson).reduce((sequence, chapterPromise) => {
return sequence.then(() => chapterPromise).then(chapter => {
console.log(`Add chapter ${chapter.chapter}`)
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
}).catch(() => {
addTextToPage(`Error occurred. ${err.message}`);
}).then(() => {
document.querySelector('.spinner').style.display = 'none';
});
}