Javascript之Promise
Promise的意思是约定,相对应的词是Defer,是推迟。他的用意是在异步的JavaScript世界引入同步写法。看看下面的“邪恶金字塔”。
var fs = require('fs');
fs.readFile('sample01.txt', 'utf8', function (err, data) {
fs.readFile('sample02.txt', 'utf8', function (err,data) {
fs.readFile('sample03.txt', 'utf8', function (err, data) {
fs.readFile('sample04.txt', 'utf8', function (err, data) {
});
});
});
});
既难看又难用。有了Promise以后,这种嵌套的金字塔代码就变成平级的链式代码。而且可以保证调用的顺序。
JavaScript官方在2013年正式宣布支持Promise语法,尽管在很多浏览器端,仍然有不兼容的情况出现。不过可见这是大势所趋。在官方宣布支持之前,已经有很多第三方库做了polyfill,就是对原有函数进行了包装,以使其支持Promise。例如下面的库:
- Q
- when
- WinJS
- RSVP.js
上面这些库和 JavaScript 原生 Promise 都遵守一个通用的、标准化的规范:Promises/A+。jQuery的Promise-Defer的实现不符合这个标准,使用需谨慎。
另外各种Promise的实现虽概念一致,但仍不尽相同。看了很多文章,仍然云里雾里。刚开始接触的是这一篇文章:
- ES6 JavaScript Promise的感性认知
讲的有点啰嗦,文末罗列的参考文献看起来还不错。后来陆陆续续又google了很多文章,讲的也不尽相同,理解不能。再后来看到了下面这一篇文章加一本在线书,感觉很不错。 - JavaScript Promises
- JavaScript Promise迷你书(中文版)
Promise的术语
- 肯定(fulfilled)
- 该 Promise 对应的操作成功了
- 否定(rejected)
- 该 Promise 对应的操作失败了
- 等待(pending)
- 还没有得到肯定或者否定结果,进行中
- 结束(settled)
- 已经肯定或者否定了
- 类 Promise (thenable)
- 一个对象是否是“类 Promise”(拥有名为“then”的方法)的。
初始状态是pending,settled=fulfilled or rejected,状态转换只有一次,而且不能逆转。
var promise = new Promise(function(resolve, reject) {
// 做一些异步操作的事情,然后……
if (/* 一切正常 */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
})
promise.then(function(result) {
console.log(result); // “完美!”
}, function(err) {
console.log(err); // Error: "出问题了"
});;
构造函数
构造器接受一个函数作为参数,它会传递给这个回调函数两个变量 resolve 和 reject。在回调函数中做一些异步操作,成功之后调用 resolve,否则调用 reject。
上面这段话的意思是,
- 首先,Promise对象用一个回调函数来构造。
- 其次,看下面这段code
- 这个回调函数有两个参数,这两个参数由Promise来负责传递(resolve& reject)。
- 回调函数中定义一些自己需要做的异步操作(asyncOp)。
- 异步操作结束时,自行判断成功失败,失败调用reject函数,成功调用resolve函数。
- Promise使用时至少与一个then配对,在then里面做asyncOp之后想做的事情。Promise里面的结果通过resolve和reject传递的参数(err&res)传到then里。
function polyFill(){
return new Promise(function (resolve, reject){
fs.asyncOp(function (err, res){
if (err)
reject(err);
else
resolve(res);
});
});
}
polyFill().then(resolveFunc, rejectFunc);
resolve & reject
这两个是Promise类的成员函数,使用的时候,也可以像下面这样调用:
promise.resolve(val).then(...);
promise.reject(Error(err)).then(...);
这样的调用和下面的常规调用是等效的:
var promise = Promise(function(resolve, reject){
if (good){
resolve(val);
}
else {
reject(Error(err));
}
});
promise.then(...);
各路贴子和解说中都鲜少提及resolve和reject函数的参数是什么,有什么用。我之前引用的两个链接都有说到,但我感觉讲的都不是很清楚。下面理一下我的思路:
resolve
- resolve总是返回一个Promise对象
- 接收到promise对象参数的时候, 返回的还是接收到的promise对象,而且下一个then会等到这个Promise定义的异步操作结束时才会运行。
- 接收到thenable类型的对象的时候, 返回一个新的promise对象,这个对象具有一个 then 方法
- 接收的参数为其他类型的时候(包括JavaScript对或null等), 返回一个将该对象作为值的新promise对象,这个值会被当作参数传到下一个定义了resolve函数参数的then里面。
reject
- reject与resolve类似,返回的也是一个Promise对象
- reject通常传入一个
Error()
对象。这不是必须的,但Error对象包含了调用栈,方便调试。
错误处理
上面提到了reject已经涉及到一部分错误处理了。在Promise里面,我们还可以用catch。
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
});
在JavaScript Promises这里提到:
这里的 catch 并无任何特殊之处,只是 then(undefined, func) 的语法糖衣,更直观一点而已。catch相当于:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
});
总结一下:
在Promise中的任何错误,包括:
- reject调用
- Promise中的抛出的异常
会落到Promise后面第一个定义了reject函数参数的then分支里。
创建序列
在JavaScript Promises中,详细解释了如何操作一组Promise,从真正的用一组Promise到使用Promise.all。这里讲的很好了,我直接拷过来。
- 最直接的数组forEach
// 从一个完成状态的 Promise 开始
var sequence = Promise.resolve();
// 遍历所有章节的 url
story.chapterUrls.forEach(function(chapterUrl) {
// 从 sequence 开始把操作接龙起来
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
});
- 用 array.reduce 精简一下上面的代码:
// 遍历所有章节的 url
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// 从 sequence 开始把操作接龙起来
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
- 我们希望同时下载所有章节,全部完成后一次搞定,正好就有这么个 API:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
});
然而这样还是有提高空间。当第一章内容加载完毕我们可以立即填进页面,这样用户可以在其他加载任务尚未完成之前就开始阅读;当第三章到达的时候我们不动声色,第二章也到达之后我们再把第二章和第三章内容填入页面,以此类推。
- 为了达到这样的效果,我们同时请求所有的章节内容,然后创建一个序列依次将其填入页面:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// 把章节 URL 数组转换成对应的 Promise 数组
// 这样就可以并行加载它们
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// 使用 reduce 把这些 Promise 接龙
// 以及将章节内容添加到页面
return sequence.then(function() {
// 等待当前 sequence 中所有章节和本章节的数据到达
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// 捕获过程中的任何错误
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
});