Promise实战
在很多需要与数据打交道的场合下,我们会遇到很多异步的情况,在异步操作的时候,我们还需要处理成功和失败两种情况,并且成功的时候,还可能需要把结果传递给下一个Ajax调用,从而形成"函数嵌套"的情况。callback是编写Javascript异步代码最最最简单的机制。可用这种原始的callback必须以牺牲控制流、异常处理和函数语义为代价。
具体实现的库
Polyfill
只需要在浏览器中加载Polyfill类库,就能使用IE10等或者还没有提供对Promise支持的浏览器中使用Promise里规定的方法。
- jakearchibald/es6-promise:一个兼容 ES6 Promises 的Polyfill类库。 它基于 RSVP.js 这个兼容 Promises/A+ 的类库, 它只是 RSVP.js 的一个子集,只实现了Promises 规定的 API。
- yahoo/ypromise: 这是一个独立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性。
- getify/native-promise-only: 以作为ES6 Promises的polyfill为目的的类库 它严格按照ES6 Promises的规范设计,没有添加在规范中没有定义的功能。 如果运行环境有原生的Promise支持的话,则优先使用原生的Promise支持。
扩展类库
Promise扩展类库除了实现了Promise中定义的规范之外,还增加了自己独自定义的功能。
- then/promise: a super set of ES6 Promises designed to have readable, performant code and to provide just the extensions that are absolutely necessary for using promises today.
- petkaantonov/bluebird: 这个类库除了兼容 Promise 规范之外,还扩展了取消promise对象的运行,取得promise的运行进度,以及错误处理的扩展检测等非常丰富的功能,此外它在实现上还在性能问题下了很大的功夫。
- when: 大小很小,node和浏览器环境下都可以使用。
- q: 类库 Q 实现了 Promises 和 Deferreds 等规范。 它自2009年开始开发,还提供了面向Node.js的文件IO API Q-IO 等, 是一个在很多场景下都能用得到的类库。
编写Promise代码
创建流程
- 使用new Promise(fn)返回一个Promise对象
- 在fn中制定异步等处理:
最基本的情况,是使用new Promise()
来创建Promise对象。也可以使用Promise.resolve(value)
代替new Promise()
快捷方法。比如:
Promise.resolve(42);
// 等价于
new Promise(function(resolve) {
reslove(42);
})
Thenable
就像我们有时称具有 .length
方法的非数组对象为Array like一样,thenable指的是一个具有 .then
方法的对象。
这种将thenable对象转换为promise对象的机制要求thenable对象所拥有的 then 方法应该和Promise所拥有的 then 方法具有同样的功能和处理过程,在将thenable对象转换为promise对象的时候,还会巧妙的利用thenable对象原来具有的 then 方法。变成了promise对象的话,就能直接使用 then 或者 catch, 比如:
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
promise.then(function(value){
console.log(value);
});
需要注意的是:即使一个对象具有 .then 方法,也不一定就能作为ES6 Promises对象使用。比如jQuery的Defeered Object
的then方法机制与Promise不同。
其实在Promise里可以将任意个方法连在一起作为一个方法链(method chain),比如:
aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
console.log(error);
});
Promise.reject
Promise.reject(error)
是和 Promise.resolve(value)
类似的静态方法,是 new Promise()
方法的快捷方式。
Promise.reject(new Error("出错了"));
// 等价于
new Promise(function(resolve,reject){
reject(new Error("出错了"));
});
Promise的同步or异步调用
先看下面这段代码:
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
这段代码会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。这实际上会让我们的代码是同步还是一部产生混淆,所以为了解决这个问题,我们应该统一使用异步调用的方式:
function onReady(fn) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
我们看到的 promise.then
也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式 ,修改代码如下:
function onReadyPromise() {
return new Promise(function (resolve, reject) {
var readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function () {
console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');
Promise#catch
链式上的catch会捕获前面所有then的错误情况。其实这也是个语法糖:
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
console.error(error);
});
// 等价于
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
console.error(error);
});
提倡使用catch的原因还有一个就是: 使用promise.then(onFulfilled, onRejected) 的话, 在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
然而实际上不管是 then 还是 catch 方法调用,都返回了一个新的promise对象。
Promise chain
通过then
方法,我们可以将代码写成方法链的形式。比如:
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
chain的时候,如何传递参数?答案非常简单,那就是在 Task A 中 return 的返回值,会在 Task B 执行时传给它。因为return的值会由 Promise.resolve(return的返回值); 进行相应的包装处理。
多个Promise对象完成后统一处理
通过回调方式来进行多个异步调用
看代码:
function getURLCallback(URL, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
callback(null, req.responseText);
} else {
callback(new Error(req.statusText), req.response);
}
};
req.onerror = function () {
callback(new Error(req.statusText));
};
req.send();
}
// <1> 对JSON数据进行安全的解析
function jsonParse(callback, error, value) {
if (error) {
callback(error, value);
} else {
try {
var result = JSON.parse(value);
callback(null, result);
} catch (e) {
callback(e, value);
}
}
}
// <2> 发送XHR请求
var request = {
comment: function getComment(callback) {
return getURLCallback('http://azu.github.io/promises-book/json/comment.json', jsonParse.bind(null, callback));
},
people: function getPeople(callback) {
return getURLCallback('http://azu.github.io/promises-book/json/people.json', jsonParse.bind(null, callback));
}
};
// <3> 启动多个XHR请求,当所有请求返回时调用callback
function allRequest(requests, callback, results) {
if (requests.length === 0) {
return callback(null, results);
}
var req = requests.shift();
req(function (error, value) {
if (error) {
callback(error, value);
} else {
results.push(value);
allRequest(requests, callback, results);
}
});
}
function main(callback) {
allRequest([request.comment, request.people], callback, []);
}
// 运行的例子
main(function(error, results){
if(error){
return console.error(error);
}
console.log(results);
});
缺点:
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
// [] 用来保存初始化的值
var pushValue = recordValue.bind(null, []);
return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 运行的例子
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
这种方法也不是我们期望的,和上面的回调函数风格相比:
Promise.all
接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then
方法。比如:
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
return Promise.all([request.comment(), request.people()]);
}
// 运行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.log(error);
});
这样的优点是:
- main中的处理流程非常清晰
- Promise.all 接收 promise对象组成的数组作为参数
Promise数组是同时开始执行的,then调用参数的结果之中的results顺序和传递的数组的顺序一致。并且调用then的时间由最后一个完成的异步操作决定。
Promise.race
它的使用方法和Promise.all一样,接收一个promise对象数组为参数。与all的区别就是:race只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。但是Promise中的数组也还是会继续执行。但是then只接受第一个完成的Promise返回对象。
参考资料
原文: https://leohxj.gitbooks.io/front-end-database/content/javascript-asynchronous/use-promise.html