promise
title: promise总结
总结在前
前言
下文类似 Promise#then、Promise#resolve 都是Promise的实例对象,
什么是Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件
Promise简介
目前大致有下面三种类型:
-
1、Constructor
var promise = new Promise(function(resolve, reject) {
// 异步处理
// 处理结束后、调用resolve 或 reject
}); -
2、Instance Method
promise.then(resolved, rejected)
.catch(rejected)
.finally() -
3、Static Method
Promise.all()
Promise.resolve()
Promise.reject()
Promise.race()
对比
Promise.resolve()
等价于
new Promise(function(resolve, null){})
Promise的状态
用new Promise 实例化的promise对象有以下三个状态。
| ES6 Promises 规范中定义的术语 | Promises/A+ 中描述状态的术语 | 状态说明 |
|---|---|---|
| has-resolution | Fulfilled | resolve(成功)时。此时会调用 onFulfilled |
| has-rejection | Rejected | reject(失败)时。此时会调用 onRejected |
| unresolved | Pending | 既不是resolve也不是reject的状态。也就是promise对象刚被创建后的初始化状态等 |
触发Promise之后只会是 resolved 或 rejected 两种,而且状态不可逆

编写Promise
创建Promise对象
-
1.new Promise(fn) 返回一个promise对象
-
2.在fn 中 指定 异步等处理
处理结果正常的话,调用resolve(处理结果值)
处理结果错误的话,调用reject(Error对象) -
3.函数处理 Promise对像
then(); --- 为了避免上述中同时使用同步、异步调用可能引起的混乱问题
catch(); --- catch在ie8下存在兼容性问题
finally();
实战
function getURL (URL) {
return new Promise(function(resolve, reject){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(req.statusText)
}
};
req.onerror = function () {
reject(new Error(req.statusText));
}
req.send();
})
}
实战 Promise中各种方法
Promise.resolve()
返回值是一个promise对象
静态方法 Promise.resolve() 可以认为是实例 new Promise(function(resolve, undefined){})方法 的快捷方式(语法糖)
- 让promise对象立即进入resolved状态
将 thenable 对象转换为promise对象
- 返回值是 thenable
实例:
Promise.resolve(value).then(function(value){
console.log(value);
})
Promise.reject()
返回值是一个promise对象
静态方法 Promise.resolve() 可以认为是实例 promise.then(undefined, onRejected)方法 的快捷方式(语法糖)
- 让promise对象立即进入rejected状态
- 使用catch()处理异常
例如:
Promise.reject(new Error("BOOM!")).catch(function(error){
console.error(error);
});
专栏: Promise只能进行异步操作?
Promise在规范上规定 Promise只能使用异步调用方式思考一个问题:文档的加载顺序都是从上到下加载的,
那么代码的的位置起何等作用?
先看看Promise的执行
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value){
console.log(value); // 3
});
console.log("outer promise"); // 2
执行顺序: 1 > 2 > 3
同步调用和异步调用同时存在导致的混乱
1.正常代码同时存在 同步 和 异步 的情况:
function onReady (fn) {
const readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function(){
console.log('loaded!')
});
console.log('==starting==');
分析:
如果在调用onReady之前DOM已经载入的话
对回调函数进行同步调用
如果在调用onReady之前DOM还没有载入的话
通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用
因此
如果这段代码在源文件中出现的位置不同,在控制台上打印的log消息顺序也会不同。
2.使用 setTimeout() 控制代码 统一异步输出的情况:
function onReady (fn) {
const readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function(){
console.log('loaded!')
});
console.log('==starting==');
关于同步 | 异步这个问题,在 Effective JavaScript 的 第67项 不要对异步回调函数进行同步调用 中也有详细介绍
- 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用
- 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果
- 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题
- 如果想在将来某时刻调用异步回调函数的话,可以使用setTimeout等异步API
Effective JavaScript
---David Herman
前面我们看到的 promise.then 也属于此类,为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式
3.使用Promise重写onReady()
function onReadyPromise () {
return new Promise(function(resolve, reject){
const readyState = document.readtState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function(){
console.log('loaded!);
});
console.log('==starting==');
由于Promise保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout 来自己实现异步调用。
Promise#then
Promise方法链(Promise Chain)。Promise可以将任意方法写在一起作为一个方法链,所以适合编写异步处理较多的应用.(越短越好)
如:
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);
无异常依次输出:Task A > Task B > Final Task
有异常依次输出: error > Final Task

Promise Chain 中的传参原理
Promise Chain 之间的传参,只需要在每个事务回调函数中使用 return 来返回当前值
如:
function doubleUp(value) {
return value * 2;
}
function increment(value) {
value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出现异常的时候会被调用
console.error(error);
});

特点:
- 每个方法中 return 的值不仅只局限于字符串或者数值类型,也可以是对象或者promise对象等复杂类型。
- return的值会由 Promise.resolve(return的返回值); 进行相应的包装处理,因此不管回调函数中会返回一个什么样的值,最终 then 的结果都是 返回 一个 新创建的promise对象。
- 也就是说, Promise#then 不仅仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,创建并返回一个promise对象。
Promise#catch
IE8兼容性

这段代码在ie8一下出现 identifier not found 的语法错误:
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
console.error(error);
});
这是怎么回事呢? 实际上这和 catch 是ECMAScript的 保留字 (Reserved Word)有关。
在ECMAScript 3中保留字是不能作为对象的属性名使用的。 而IE8及以下版本都是基于ECMAScript 3实现的,因此不能将 catch 作为属性来使用,也就不能编写类似 promise.catch() 的代码,因此就出现了 identifier not found 这种语法错误了。
而现在的浏览器都是基于ECMAScript 5的,而在ECMAScript 5中保留字都属于 IdentifierName ,也可以作为属性名使用了。
在ECMAScript5中保留字也不能作为 Identifier 即变量名或方法名使用。 如果我们定义了一个名为 for 的变量的话,那么就不能和循环语句的 for 区分了。 而作为属性名的话,我们还是很容易区分 object.for 和 for 的,仔细想想我们就应该能接受将保留字作为属性名来使用了。
分析
点标记法(dot notation) 要求对象的属性必须是有效的标识符(在ECMAScript 3中则不能使用保留字),
但是使用 中括号标记法(bracket notation)的话,则可以将非合法标识符作为对象的属性名使用。
也就是说,上面的代码如果像下面这样重写的话,就能在IE8及以下版本的浏览器中运行了(当然还需要polyfill)。
解决ie8一下兼容性问题
1.使用中括号标记法(bracket notation):
解决Promise#catch标识符冲突问题
var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
console.error(error);
});
2.使用Promise#then 替代Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
console.error(error);
});
科普:一些库有关promise.catch的解决方案
由于 catch 标识符可能会导致问题出现,因此一些类库(Library)也采用了 caught 作为函数名,而函数要完成的工作是一样的。
而且很多压缩工具自带了将 promise.catch 转换为 promise["catch"] 的功能, 所以可能不经意之间也能帮我们解决这个问题。
如果各位读者需要支持IE8及以下版本的浏览器的话,那么一定要将这个 catch 问题牢记在心中。
专栏: 每次调用then 都会 返回 一个新创建的promise对象
测试每次调用then的返回值
var aPromise = new Promise(function (resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
因此每次都会返回不同Promise对象
所以要留心没使用Promise时,若要扩展方法时需要返回新的Promise对象进行处理
实战
// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log("1: " + value); // => 100
})
// vs
// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("2: " + value); // => 100 * 2 * 2
});
对比可看出
- 不写成链式情况
promise.then每次接受的参数都为第一次new时所传入的数,
因此,多次promise.then这成立一个异步事务流
- 写成链式情况
写成链式之后,由于每次promise.then都会返回一个新的promise对象,
则每个then可以通过链式调用来return当前then返回的数据或对象
因此,链式调用是比较推荐的写法,能避免很多不必要的错误
一个有关then的很有代表性的反模式的例子
错误做法xxx
function badAsyncCall() {
const promise = Promise.resolve(value);
promise.then(function(value){
return value;
});
return promise;
}
正确做法✔✔✔
function goodAsyncCall() {
const promise = Promise.resolve(value);
return promise.then(function(value){
return value;
});
}
这种函数的行为贯穿在Promise整体之中, 包括我们后面要进行说明的 Promise.all 和 Promise.race ,他们都会接收一个promise对象为参数,并返回一个和接收参数不同的、新的promise对象。
Promise和数组
因为技术不够,暂时总结不出来,先贴代码
需要事先说明的是 Promise.all 比较适合这种应用场景的需求,因此我们故意采用了大量 .then 的晦涩的写法。
使用了.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() {
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){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.statusText);
} else {
reject(req.statusText);
}
};
req.onerror = function() {
reject(req.statusText);
}
});
}
const 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/comment.json').then(JSON.parse);
}
};
function main () {
return Promise.all([request.comment(), request.people()]);
}
main().then(function(value){
console.log(value);
}).catch(function(value){
console.log(value);
});
Promise.race
使用方法和Promise.all一样,接收一个promise对象数组为参数。
Promise.all 在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理,
与之相对的是 Promise.race 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。
实例:
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is winner');
resolve('this is winner');
}, 4);
});
var loserPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is loser');
resolve('this is loser');
}, 1000);
});
// 第一个promise变为resolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
陆续输出: this is winner -> this is loser
也就是说, Promise.race 在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。
在 ES6 Promises 规范中,也没有取消(中断)promise对象执行的概念,我们必须要确保promise最终进入resolve or reject状态之一。也就是说Promise并不适用于 状态 可能会固定不变的处理。也有一些类库提供了对promise进行取消的操作。
then or catch?
.then 和 .catch 都会创建并返回一个 新的 promise对象。 Promise实际上每次在方法链中增加一次处理的时候所操作的都不是完全相同的promise对象。

案例:
function throwError(value) {
// 抛出异常
throw new Error(value);
}
// <1> onRejected不会被调用
function badMain(onRejected) {
return Promise.resolve(42).then(throwError, onRejected);
}
// <2> 有异常发生时onRejected会被调用
function goodMain(onRejected) {
return Promise.resolve(42).then(throwError).catch(onRejected);
}
// 运行示例
badMain(function(){
console.log("BAD");
});
goodMain(function(){
console.log("GOOD");
});
输出结果:“GOOD”,
而且badMain没有运行
分析
在上述例子中,Promise.resolve(42)接受了value后返回 一个新的Promise对象A,接着then通过链式接受A对象并分别指定回调函数里面的参数resolve和reject;
对于badMain函数,then方法回调函数参数里面的的resolve传入一个threwError函数E并抛出异常,在回调参数里面的的resolve传入onRejected函数。因为异常函数传入then参数的resolve,reosolve抛出的异常需要在下一个then的参数reject 或 在then后面的catch来接收异常,所以badMain函数没有被调用;
对于goodMain函数,then方法回调函数参数的resolve抛出异常,并有then后面的catch函数接受异常函数E,所以goodMain被调用并正常打印成功。
总结
这里我们又学习到了如下一些内容。
1.使用promise.then(onFulfilled, onRejected) 的话
- 在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
2.在 promise.then(onFulfilled).catch(onRejected) 的情况下
- then 中产生的异常能在 .catch 中捕获
3..then 和 .catch 在本质上是没有区别的
- 需要分场合使用。
我们需要注意如果代码类似 badMain 那样的话,就可能出现程序不会按预期运行的情况,从而不能正确的进行错误处理。
Promise测试
总结编写Promise 的测试代码
1.使用Mocha测试框架
现在使用 Mocha来对Promise 进行基本的测试
使用Mocha的主要基于下面3点理由:
- 它是非常著名的测试框架
- 支持基于Node.js 和浏览器的测试
- 支持"Promise测试"
Macha简介
Mocha可以自由选择BDD、TDD、exports中的任意风格,测试中用到的Assert 方法也同样可以跟任何其他类库组合使用。 也就是说,Mocha本身只提供执行测试时的框架,而其他部分则由使用者自己选择
- Mocha官网
- Mocha是Node.js下的测试框架工具
Macha-回调函数风格的测试
//basic-test.js
var assert = require('power-assert');
describe('Basic Test', function () {
context('When Callback(high-order function)', function () {
it('should use `done` for test', function (done) {
setTimeout(function () {
assert(true);
done();
}, 0);
});
});
context('When promise object', function () {
it('should use `done` for test?', function (done) {
var promise = Promise.resolve(1);
// このテストコードはある欠陥があります
promise.then(function (value) {
assert(value === 1);
done();
});
});
});
});
Promise进阶(Advanced)
Promise的实现类库(Library)
ES6 Promise 里关于promise对象的规定包括在使用 catch 方法,或使用 Promise.all 进行处理的时候不能出现错误。
Promises/A+ 是 ES6 Promises 的前身,Promise的 then 也是来自于此的基于社区的规范。
如果说一个类库兼容 Promises/A+ 的话,那么就是说它除了具有标准的 then 方法之外,很多情况下也说明此类库还支持 Promise.all 和 catch 等功能。
但是 Promises/A+ 实际上只是定义了关于 Promise#then 的规范,所以有些类库可能实现了其它诸如 all 或 catch 等功能,但是可能名字却不一样。
如果我们说一个类库具有 then 兼容性的话,实际上指的是 Thenable ,它通过使用 Promise.resolve 基于ES6 Promise的规定,进行promise对象的变换。
Polyfill和扩展类库
在这些Promise的实现类库中,我们这里主要对两种类型的类库进行介绍。
-
一种是被称为 Polyfill (这是一款英国产品,就是装修刮墙用的腻子,其意义可想而知 — 译者注)的类库,另一种是即具有 Promises/A+兼容性 ,又增加了自己独特功能的类库。
-
Promise的实现类库数量非常之多,这里我们只是介绍了其中有限的几个。
1.Polyfill
只需要在浏览器中加载Polyfill类库,就能使用IE10等或者还没有提供对Promise支持的浏览器中使用Promise里规定的方法。
也就是说如果加载了Polyfill类库,就能在还不支持Promise的环境中,运行本文中的各种示例代码。
- jakearchibald/es6-promise
一个兼容 ES6 Promises 的Polyfill类库。 它基于 RSVP.js 这个兼容 Promises/A+ 的类库, 它只是 RSVP.js 的一个子集,只实现了Promises 规定的 API。 -
yahoo/ypromise
这是一个独立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性。 本书的示例代码也都是基于这个 ypromise 的 Polyfill 来在线运行的。 -
getify/native-promise-only
以作为ES6 Promises的polyfill为目的的类库 它严格按照ES6 Promises的规范设计,没有添加在规范中没有定义的功能。 如果运行环境有原生的Promise支持的话,则优先使用原生的Promise支持。
2.Promise扩展类库
Promise扩展类库除了实现了Promise中定义的规范之外,还增加了自己独自定义的功能。
Promise扩展类库数量非常的多,我们只介绍其中两个比较有名的类库。
Q 和 Bluebird 这两个类库除了都能在浏览器里运行之外,充实的API reference也是其特征。
-
kriskowal/q
类库 Q 实现了 Promises 和 Deferreds 等规范。 它自2009年开始开发,还提供了面向Node.js的文件IO API Q-IO 等, 是一个在很多场景下都能用得到的类库。 -
API Reference · kriskowal/q Wiki
Q等文档里详细介绍了Q的Deferred和jQuery里的Deferred有哪些异同,以及要怎么进行迁移 Coming from jQuery 等都进行了详细的说明。 -
petkaantonov/bluebird
这个类库除了兼容 Promise 规范之外,还扩展了取消promise对象的运行,取得promise的运行进度,以及错误处理的扩展检测等非常丰富的功能,此外它在实现上还在性能问题下了很大的功夫。 -
bluebird/API.md at master · petkaantonov/bluebird
Bluebird的文档除了提供了使用Promise丰富的实现方式之外,还涉及到了在出现错误时的对应方法以及 Promise中的反模式 等内容。
这两个类库的文档写得都很友好,即使我们不使用这两个类库,阅读一下它们的文档也具有一定的参考价值。
Promise.resolve和Thenable
Promise.resolve 的最大特征之一就是可以将thenable的对象转换为promise对象。现在总结一下利用将thenable对象转换为promise对象这个功能都能具体做些什么事情。下面是案例:
1.将Web Notifications转换为thenable对象
使用方法:
new Notification("Hi!");
Notification API 的使用步骤:
用户在这个是否允许Notification的对话框选择后的结果,会通过 Notification.permission 传给我们的程序,它的值可能是允许("granted")或拒绝("denied")这二者之一。
-
显示是否允许通知的对话框,并异步处理用户选择结果。resolve(成功)时 == 用户允许("granted")
-
如果用户允许的话,则通过 new Notification 显示通知消息。这又分两种情况
-
用户之前已经允许过
-
-
当场弹出是否允许桌面通知对话框。reject(失败)时 == 用户拒绝("denied")
- 当用户不允许的时候,不执行任何操作
实战:
1.以回调函数编写为例
notification-callback.js
funtion notifyMessage(msg, opt, callback) {
if(Notification && Notification.permission === 'granted'){
const notification = new Notification(msg, opt);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 运行实例
// 第二个参数是传给 `Notification` 的option对象
notifyMessage("Hi!", {}, function (error, notification) {
if(error){
return console.error(error);
}
console.log(notification);// 通知对象
});
2.对比使用回调函数,下面以promise重写
notification-as-promise.js
function notifyMessage(msg, opt, callback) {
if (Notification && Notification.permission === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function(status){
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
function notifyMessagePromise(msg opt) {
return new Promise(function(resolve, reject){
notifyMessage(msg, opt, function(error, notification){
if (error) {
reject(error);
} else {
resolve(notification);
}
});
});
}
notifyMessagePromise("Hi!").then(function(notification){
console.log(notification);
}).catch(function(error){
console.log(error);
});
3.Web Notifications As Thenable。thenable就是一个具有 .then方法的一个对象
notification-thenable.js
function notifyMessage(message, options, callback) {
if (Notification && Notification.permission === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 返回 `thenable`
function notifyMessageAsThenable(msg, opt) {
return {
'then': function(resolve, reject){
notifyMessage(msg, opt, function(error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
}
};
}
// 运行实例
Promise.resolve(notifyMessageAsThenable('Hi!', opt)).then(function(notification){
console.log(notification);
}).catch(function(error){
consoe.log(error);
});
notification-thenable.js里增加了一个 notifyMessageAsThenable方法。这个方法返回的对象具备一个then方法。
then方法的参数和 new Promise(function (resolve, reject){}) 一样,在确定时执行 resolve 方法,拒绝时调用 reject 方法。
then 方法和 notification-as-promise.js 中的 notifyMessageAsPromise 方法完成了同样的工作。
我们可以看出, Promise.resolve(thenable) 通过使用了 thenable 这个promise对象,就能利用Promise功能了。
Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
console.log(notification);// 通知对象
}).catch(function(error){
console.error(error);
});
使用了Thenable的notification-thenable.js 和依赖于Promise的 notification-as-promise.js ,实际上都是非常相似的使用方法。
notification-thenable.js 和 notification-as-promise.js比起来,有以下的不同点:
- 类库侧没有提供 Promise 的实现
- 用户通过 Promise.resolve(thenable) 来自己实现了 Promise
- 作为Promise使用的时候,需要和 Promise.resolve(thenable) 一起配合使用
通过使用Thenable对象,我们可以实现类似已有的回调式风格和Promise风格中间的一种实现风格。
4.总结
在本小节我们主要学习了什么是Thenable,以及如何通过Promise.resolve(thenable) 使用Thenable,将其作为promise对象来使用。
Callback — Thenable — Promise
Thenable风格表现为位于回调和Promise风格中间的一种状态,作为类库的公开API有点不太成熟,所以并不常见。
Thenable本身并不依赖于Promise功能,但是Promise之外也没有使用Thenable的方式,所以可以认为Thenable间接依赖于Promise。
另外,用户需要对 Promise.resolve(thenable) 有所理解才能使用好Thenable,因此作为类库的公开API有一部分会比较难。和公开API相比,更多情况下是在内部使用Thenable。
在编写异步处理的类库的时候,推荐采用先编写回调风格的函数,然后再转换为公开API这种方式。
貌似Node.js的Core module就采用了这种方式,除了类库提供的基本回调风格的函数之外,用户也可以通过Promise或者Generator等自己擅长的方式进行实现。
最初就是以能被Promise使用为目的的类库,或者其本身依赖于Promise等情况下,我想将返回promise对象的函数作为公开API应该也没什么问题。
什么时候该使用Thenable?
那么,又是在什么情况下应该使用Thenable呢?
恐怕最可能被使用的是在 Promise类库 之间进行相互转换了。
比如,类库Q的Promise实例为Q promise对象,提供了 ES6 Promises 的promise对象不具备的方法。Q promise对象提供了 promise.finally(callback) 和 promise.nodeify(callback) 等方法。
如果你想将ES6 Promises的promise对象转换为Q promise的对象,轮到Thenable大显身手的时候就到了。
使用thenable将promise对象转换为Q promise对象
var Q = require("Q");
// 这是一个ES6的promise对象
var promise = new Promise(function(resolve){
resolve(1);
});
// 变换为Q promise对象
Q(promise).then(function(value){
console.log(value);
}).finally(function(){
console.log("finally");
});
因为是Q promise对象所以可以使用 finally 方法
上面代码中最开始被创建的promise对象具备then方法,因此是一个Thenable对象。我们可以通过Q(thenable)方法,将这个Thenable对象转换为Q promise对象。
可以说它的机制和 Promise.resolve(thenable) 一样,当然反过来也一样。
像这样,Promise类库虽然都有自己类型的promise对象,但是它们之间可以通过Thenable这个共通概念,在类库之间(当然也包括native Promise)进行promise对象的相互转换。
我们看到,就像上面那样,Thenable多在类库内部实现中使用,所以从外部来说不会经常看到Thenable的使用。但是我们必须牢记Thenable是Promise中一个非常重要的概念。
使用reject而不是throw
Promise的构造函数,以及被 then 调用执行的函数基本上都可以认为是在 try...catch 代码块中执行的,所以在这些代码中即使使用 throw ,程序本身也不会因为异常而终止。
那么使用reject还是throw?先看看代码对比
// throw实例
const promise = new Promise(function(resolve, reject){
throw new Error('error');
});
promise.catch(function(error){
console.log(error);
});
如果在Promise中使用 throw 语句的话,会被 try...catch 住,最终promise对象也变为Rejected状态
// reject实例
const promise = new Promise(function(resolve, reject){
reject(new Error("message"));
});
promise.catch(function(error){
console.error(error);// => "message"
})
对比上面案例,相对于throw,使用reject更加直观、合理
使用reject有什么优点?
1.首先是因为我们很难区分 throw 是我们主动抛出来的,还是因为真正的其它 异常 导致的。
- 比如在使用Chrome浏览器的时候,Chrome的开发者工具提供了在程序发生异常的时候自动在调试器中break的功能。
~

当我们开启这个功能的时候,在执行到下面代码中的 throw 时就会触发调试器的break行为。
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
本来这是和调试没有关系的地方,也因为在Promise中的 throw 语句被break了,这也严重的影响了浏览器提供的此功能的正常使用。
2.在then中进行reject
关于使用Promise进行超时处理的具体实现方法可以参考 使用Promise.race和delay取消XHR请求 中的详细说明。
现在用then来处理超时
const promise = Promise.resolve()l
promise.then(function(){
const retPromise = new Promise(function(resolve, reject){
setTimeout(function(){
// 经过一段时间后还没处理完的话就进行reject
});
// 比较耗时的处理 - 1
somethingHardWork();
});
return retPromise;
}).then(onFulfilled, onRejected);
原理分析:因为then在注册回调函数的时候可以通过return一个值或对象来传给后面的then或catch中的回调函数处理,因此可以使用then中return的特性来解决。提倡使用Promise.race和delay取消XHR请求
3.总结
- 使用 reject 会比使用 throw 安全
- 在 then 中使用reject的方法
Deferred和Promise
这里的Deferred指具有js延迟的一类库(Library)
1.Deferred和Promise的关系
- Deferred 拥有 Promise
- Deferred 具备对 Promise的状态进行操作的特权方法(图中的"特権メソッド")

我想各位看到此图应该就很容易理解了,Deferred和Promise并不是处于竞争的关系,而是Deferred内涵了Promise。
这是jQuery.Deferred结构的简化版。当然也有的Deferred实现并没有内涵Promise。
Deferred最初是在Python的 Twisted 框架中被提出来的概念。 在JavaScript领域可以认为它是由 MochiKit.Async 、 dojo/Deferred 等Library引入的。
2.实例Deferred top on Promise
基于Promise实现的deferred
// deferred.js
function Deferred(){
this.promise = new Promise(function(resolve, reject){
this._resolve = resolve;
this._reject = reject;
}).bind(this);
}
Deferred.prototype.resolve = function(value){
this._resolve.call(this.promise, value);
}
Deferred.prototype.reject = function(reason){
this._reject.call.call(this.promise, reason);
}
使用Promise实现的 getURL 用Deferred改写一下
// xhr-deferred.js
function Deferred(){
this.promise = new Promise(function(resolve, reject){
this._resolve = resolve;
this._reject = reject;
}).bind(this);
}
Deferred.prototype.resolve = function(value){
this._resolve.call(this.promise, value);
}
Deferred.prototype.reject = function(reason){
this._reject.call.call(this.promise, reason);
}
function getURL(URL) {
const deferred = new Deferred();
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
deferred.resolve(req.reponseText);
} else {
deferred.reject(req.reponseText);
}
};
req.onerror = function() {
deferred.reject(new Error(req.statusText));
};
req.send();
return deferred.promise;
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
所谓的能对Promise状态进行操作的
特权方法,指的就是能对promise对象的状态进行resolve、reject等调用的方法,而通常的Promise的话只能在通过构造函数传递的方法之内对promise对象的状态进行操作。
我们来看看Deferred和Promise相比在实现上有什么异同。
xhr-promise.js
function getURL(URL) {
return new Promise(function(resolve, reject){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.reponseText);
} else {
reject(req.reponseText);
}
}
req.onerror = function() {
reject(new Error(req.statusText));
};
req.send();
});
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
对比上述两个版本的 getURL ,我们发现它们有如下不同。
- Deferred 的话不需要将代码用Promise括起来
- 由于没有被嵌套在函数中,可以减少一层缩进
- 反过来没有Promise里的错误处理逻辑
在以下方面,它们则完成了同样的工作。
- 整体处理流程
- 调用 resolve、reject 的时机
- 函数都返回了promise对象
由于Deferred包含了Promise,所以大体的流程还是差不多的,不过Deferred有用对Promise进行操作的特权方法,以及高度自由的对流程控制进行自由定制。
比如在Promise一般都会在构造函数中编写主要处理逻辑,对 resolve、reject 方法的调用时机也基本是很确定的。
new Promise(function (resolve, reject){
// 在这里进行promise对象的状态确定
});
而使用Deferred的话,并不需要将处理逻辑写成一大块代码,只需要先创建deferred对象,可以在任何时机对 resolve、reject 方法进行调用。
var deferred = new Deferred();
// 可以在随意的时机对 `resolve`、`reject` 方法进行调用
上面我们只是简单的实现了一个 Deferred ,我想你已经看到了它和 Promise 之间的差异了吧。
如果说
Promise是用来对值进行抽象的话,Deferred则是对处理还没有结束的状态或操作进行抽象化的对象,我们也可以从这一层的区别来理解一下这两者之间的差异。
换句话说,Promise代表了一个对象,这个对象的状态现在还不确定,但是未来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而Deferred对象表示了一个处理还没有结束的这种事实,在它的处理结束的时候,可以通过Promise来取得处理结果。
使用Promise.race和delay取消XHR请求
当然XHR有一个 timeout 属性,使用该属性也可以简单实现超时功能,但是为了能支持多个XHR同时超时或者其他功能,我们采用了容易理解的异步方式在XHR中通过超时来实现取消正在进行中的操作。
如何使用Promise.race来实现超时机制?
1.让Promise等待指定时间
首先我们来串讲一个单纯的在Promise中调用 setTimeout 的函数。
delayPromise.js
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delayPromise(ms) 返回一个在经过了参数指定的毫秒数后进行onFulfilled操作的promise对象,这和直接使用 setTimeout 函数比较起来只是编码上略有不同,如下所示。
setTimeout(function () {
alert("已经过了100ms!");
}, 100);
// == 几乎同样的操作
delayPromise(100).then(function () {
alert("已经过了100ms!");
});
在这里 promise对象 这个概念非常重要,请切记。
2.Promise.race中的超时
它的作用是在任何一个promise对象进入到确定(解决)状态后就继续进行后续处理,如下面的例子所示
var winnerPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is winner');
resolve('this is winner');
}, 4);
});
var loserPromise = new Promise(function (resolve) {
setTimeout(function () {
console.log('this is loser');
resolve('this is loser');
}, 1000);
});
// 第一个promise变为resolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
我们可以将刚才的 delayPromise 和其它promise对象一起放到 Promise.race 中来是实现简单的超时机制。
simple-timeout-promise.js
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
函数 timeoutPromise(比较对象promise, ms) 接收两个参数,第一个是需要使用超时机制的promise对象,第二个参数是超时时间,它返回一个由 Promise.race 创建的相互竞争的promise对象。
之后我们就可以使用 timeoutPromise 编写下面这样的具有超时机制的代码了。
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
// 运行示例
var taskPromise = new Promise(function(resolve){
// 随便一些什么处理
var delay = Math.random() * 2000;
setTimeout(function(){
resolve(delay + "ms");
}, delay);
});
timeoutPromise(taskPromise, 1000).then(function(value){
console.log("taskPromise在规定时间内结束 : " + value);
}).catch(function(error){
console.log("发生超时", error);
});
虽然在发生超时的时候抛出了异常,但是这样的话我们就不能区分这个异常到底是普通的错误还是超时错误了。
为了能区分这个 Error 对象的类型,我们再来定义一个Error 对象的子类 TimeoutError。
3.定制Error对象
Error 对象是ECMAScript的内建(build in)对象。
但是由于stack trace等原因我们不能完美的创建一个继承自 Error 的类,不过在这里我们的目的只是为了和Error有所区别,我们将创建一个 TimeoutError 类来实现我们的目的。
在ECMAScript6中可以使用 class 语法来定义类之间的继承关系。
class MyError extends Error{
// 继承了Error类的对象
}
为了让我们的 TimeoutError 能支持类似 error instanceof TimeoutError 的使用方法,我们还需要进行如下工作。
TimeoutError.js
function copyOwnFrom(target, source) {
Object.getOwnPropertyNames(source).forEach(function (propName) {
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
return target;
}
function TimeoutError() {
var superInstance = Error.apply(null, arguments);
copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;
有了这个 TimeoutError 对象,我们就能很容易区分捕获的到底是因为超时而导致的错误,还是其他原因导致的Error对象了
4.通过超时取消XHR操作
到这里,我想各位读者都已经对如何使用Promise来取消一个XHR请求都有一些思路了吧。
取消XHR操作本身的话并不难,只需要调用 XMLHttpRequest 对象的 abort() 方法就可以了。
为了能在外部调用 abort() 方法,我们先对之前本节出现的 getURL 进行简单的扩展,cancelableXHR 方法除了返回一个包装了XHR的promise对象之外,还返回了一个用于取消该XHR请求的abort方法。
delay-race-cancel.js
function cancelableXHR(URL) {
var req = new XMLHttpRequest();
var promise = new Promise(function (resolve, reject) {
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.onabort = function () {
reject(new Error('abort this request'));
};
req.send();
});
var abort = function () {
// 如果request还没有结束的话就执行abort
// https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
if (req.readyState !== XMLHttpRequest.UNSENT) {
req.abort();
}
};
return {
promise: promise,
abort: abort
};
}
在这些问题都明了之后,剩下只需要进行Promise处理的流程进行编码即可。大体的流程就像下面这样:
1.通过 cancelableXHR 方法取得包装了XHR的promise对象和取消该XHR请求的方法
2.在 timeoutPromise 方法中通过 Promise.race 让XHR的包装promise和超时用promise进行竞争。
-
XHR在超时前返回结果的话
a.和正常的promise一样,通过
then返回请求结果 -
发生超时的时候
a.抛出
throw TimeoutError异常并被catchb.catch的错误对象如果是
TimeoutError类型的话,则调用abort方法取消XHR请求
将上面的步骤总结一下的话,代码如下所示
delay-race-cancel-play.js
function copyOwnFrom(target, source) {
Object.getOwnPropertyNames(source).forEach(function(propName){
Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
});
return target;
}
function TimeoutError() {
const superInstance = Error.apply(null, arguments);
copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error,prototype);
TimeoutError.prototype.constructor = TimeourError;
function delayPromise(ms){
return new Promise(function(resolve){
setTimeout(function(resolve, ms));
});
}
function timeoutPromise(promise, ms){
let timeout = delayPromise(ms).then(function(){
return Promise.reject(new TimeoutError('Operation timed out after' + ms + 'ms'));
});
return Promise.race([promise, timeout]);
}
function cancelableXHR(URL) {
const req = new XMLHttpRequest();
const promise = new Promise(function(resolve, reject){
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.statusText);
} else {
reject(req.statusText);
}
};
req.onerror = function() {
reject(new Error(req.statusText));
};
req.abort = function() {
