前言
本文主要参考了 JavaScript Promise 迷你书,链接在文末与其他参考一起列出。
promise 基础
Promise 是异步编程的一种解决方案。ES6 Promise 的规范来源于 Promises/A+社区,它有很多版本的实现。
Promise 比传统的解决方案(回调函数和事件)更合理和更强大,可以避免回调地狱。使用 Promise 来统一处理异步操作,更具语义化、易于理解、有利维护。
Promise 接口的基本思想是让异步操作返回一个 Promise 对象,我们可以对这个对象进行一些操作。
三种状态和两种变化途径
Promise 对象只有三种状态。
- 异步操作“未完成”,promise 对象刚被创建后的初始化状态(unresolved,Promises/A+中称 pending)
- 异步操作“已完成”(resolved,Promises/A+中称 fulfilled)
- 异步操作“失败”(rejected)
这三种的状态的变化途径只有两种。
- 异步操作从“未完成”到“已完成”
- 异步操作从“未完成”到“失败”。
这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise 对象的最终结果只有两种。
异步操作成功,Promise 对象传回一个值,状态变为 resolved。
异步操作失败,Promise 对象抛出一个错误,状态变为 rejected。
api 简介
目前主要有三种类型
- 构造函数(Constructor)
创建一个 promise 实例:
var promise = new Promise(function (resolve, reject) {
// 异步处理
// 处理结束后、调用resolve 或 reject
});
- 实例方法(Instance Method)
promise.then(onFulfilled, onRejected);
promise.catch(onRejected);
- 静态方法(Static Method)
Promise.all()、 Promise.race()、Promise.resolve()、Promise.reject()
创建 promise 对象
给 Promise 构造函数传递一个函数 fn 作为参数实例化即可。这个函数 fn 有两个参数(resolve 和 reject),在 fn 中指定异步等处理:
- 处理结果正常的话,调用 resolve(处理结果值)
- 处理结果错误的话,调用 reject(Error 对象)。
// 创建promise对象基本形式
var promise = new Promise(function (resolve, reject) {
// ... some code
if (/* 异步操作成功 */) {
resolve(value)
} else {
reject(error)
}
})
// 将图片加载转为promise形式
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image()
image.onload = resolve
image.onerror = reject
image.src = path
})
}
// 创建XHR的promise对象
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 URL = 'http://httpbin.org/get'
getURL(URL)
.then(function onFulfilled (value){
console.log(value)
})
.catch(function onRejected (error){
console.error(error)
})
getURL 只有在通过 XHR 取得结果状态为 200 时才会调用 resolve。也就是只有数据取得成功时,而其他情况(取得失败)时则会调用 reject 方法。
resolve(req.responseText)在 response 的内容中加入了参数。resolve 方法的参数并没有特别的规则,基本上把要传给回调函数参数放进去就可以了。(then 方法可以接收到这个参数值)
为 promise 对象添加处理方法
为 promise 对象添加处理方法主要有以下两种:
- promise 对象被 resolve 时的处理(onResolved)
- promise 对象被 reject 时的处理(onRejected)
被 resolve 后的处理,可以在.then 方法中传入想要调用的函数:
var URL = 'http://httpbin.org/get';
getURL(URL).then(function onResolved(value) {
console.log(value);
});
被 reject 后的处理,可以在.then 的第二个参数或者是在.catch 方法中设置想要调用的函数。
var URL = 'http://httpbin.org/status/500';
getURL(URL)
.then(function onResolved(value) {
console.log(value);
})
.catch(function onRejected(error) {
console.error(error);
});
.catch 只是 promise.then(undefined, onRejected)的别名而已,如下代码也可以完成同样的功能。
getURL(URL).then(onResolved, onRejected);
Promise.resolve
1)new Promise 的快捷方式
静态方法 Promise.resolve(value)可以认为是 new Promise()方法的快捷方式。Promise.resolve(value)返回一个状态由给定 value 决定的 Promise 对象。如果该值是一个 Promise 对象,则直接返回该对象;如果该值是 thenable 对象(见下面部分 2),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象),返回的 Promise 对象状态为 resolved,并且将该 value 传递给对应的 then 方法。
所以和 new Promise()方法并不完全一致。Promise.resolve 接收一个 promise 对象会直接返回这个对象。而 new Promise()总是新生成一个 promise 对象。
var p1 = Promise.resolve(1);
var p2 = Promise.resolve(p1);
var p3 = new Promise(function (resolve, reject) {
resolve(p1);
});
console.log(p1 === p2); // true
console.log(p1 === p3); // false
常用 Promise.resolve()快速初始化一个 promise 对象。
Promise.resolve(42).then(function (value) {
console.log(value);
});
2)Promise.resolve 方法另一个作用就是将 thenable 对象转换为 promise 对象。
什么是 thenable 对象?Thenable 对象可以认为是类 Promise 对象,拥有名为.then 方法的对象。和类数组的概念相似。
有哪些 thenable 对象?主要是 ES6 之前有许多库实现了 Promise,其中有很多与 ES6 Promise 规范并不一致,我们称这些与 ES6 中的 promise 对象类似而又有差异的对象为 thenable 对象。如 jQuery 中的 ajax()方法返回的对象。
// 将thenable对象转换promise对象
var promise = Promise.resolve($.ajax('/json/comment.json')); // => promise对象
promise.then(function (value) {
console.log(value);
});
Promise.reject()
Promise.reject(error)是和 Promise.resolve(value)类似的静态方法,是 new Promise()方法的快捷方式。
比如 Promise.reject(new Error(‘出错了’))就是下面代码的语法糖形式:
new Promise(function (resolve, reject) {
reject(new Error('出错了'));
});
Promise.all
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
var p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all 方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。
p 的状态由 p1、p2、p3 决定,分成两种情况。
(1)只有 p1、p2、p3 的状态都变成 resolved,p 的状态才会变成 resolved,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
(2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。
传递给 Promise.all 的 promise 并不是一个个的顺序执行的,而是同时开始、并行执行的。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
// 所有promise变为resolve后程序退出
Promise.all([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128),
]).then(function (values) {
console.log(Date.now() - startDate + 'ms');
// 约128ms
console.log(values); // [1, 32, 64, 128]
});
从上述结果可以看出,传递给 Promise.all 的 promise 并不是一个个的顺序执行的,而是同时开始、并行执行的。
如果这些 promise 全部串行处理的话,那么需要等待 1ms → 等待 32ms → 等待 64ms → 等待 128ms ,全部执行完毕需要约 225ms 的时间。
Promise.race
var p = Promise.race([p1, p2, p3]);
与 Promise.all 类似,但是只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
// 任何一个promise变为resolve或reject的话程序就停止运行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64),
timerPromisefy(128),
]).then(function (value) {
console.log(value); // => 1
});
下面我们再来看看在第一个 promise 对象变为确定(resolved)状态后,它之后的 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'
});
执行上面代码的话,我们会看到 winnter 和 loser promise 对象的 setTimeout 方法都会执行完毕,console.log 也会分别输出它们的信息。
也就是说,Promise.race 在第一个 promise 对象变为 Fulfilled 之后,并不会取消其他 promise 对象的执行。
在 ES6 Promises 规范中,也没有取消(中断)promise 对象执行的概念,我们必须要确保 promise 最终进入 resolve or reject 状态之一。也就是说 Promise 并不适用于状态可能会固定不变的处理。也有一些类库提供了对 promise 进行取消的操作。
Promise 的实现类库(Library)
由于很多浏览器不支持 ES6 Promises,我们需要一些第三方实现的和 Promise 兼容的类库。
选择 Promise 类库首先要考虑的是否具有 Promises/A+兼容性。
Promises/A+是 ES6 Promises 的前身,Promise 的 then 也是由社区的规范而来。
这些类库主要有两种:Polyfill 和扩展类库
1)Polyfill
- jakearchibald/es6-promise:应用最广泛的一个库,推荐使用这个库。
- yahoo/ypromise:这是一个独立版本的 YUI 的 Promise Polyfill。
- getify/native-promise-only:严格按照 ES6 Promises 的规范设计,没有添加在规范中没有定义的功能。
2)Promise 扩展类库
- kriskowal/q: Q.promise,这个大家应该都比较熟悉了。Angularjs 中的$q 也是受此启发。
- petkaantonov/bluebird:这个类库除了兼容 Promise 规范之外,还扩展了取消 promise 对象的运行,取得 promise 的运行进度,以及错误处理的扩展检测等非常丰富的功能,此外它在实现上还在性能问题下了很大的功夫。
Q 等文档里详细介绍了 Q 的 Deferred 和 jQuery 里的 Deferred 有哪些异同,以及要怎么进行迁移等都进行了详细的说明。
两个有用的附加方法
1)done()
Promise 对象的回调链,不管以 then 方法或 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个 done 方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
'use strict';
if (typeof Promise.prototype.done === 'undefined') {
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (error) {
setTimeout(function () {
throw error;
}, 0);
});
};
}
// 调用
asyncFunc().then(f1).catch(r1).then(f2).done();
从上面代码可以看到 done 有以下两个特点。
- done 中出现的错误会被作为异常抛出
- 终结 Promise chain
那么它是如何将异常抛到 Promise 的外面的呢?其实这里我们利用的是在 setTimeout 中使用 throw 方法,直接将异常抛给了外部。
// setTimeout的回调函数中抛出异常
try {
setTimeout(function callback() {
throw new Error('error');
}, 0);
} catch (error) {
console.error(error);
}
因为异步的 callback 中抛出的异常不会被捕获,上面例子中的例外不会被捕获。
ES6 Promises 和 Promises/A+等在设计上并没有对 Promise.prototype.done 做出任何规定,但是为什么很多类库都提供了该方法的实现呢?
主要是防止编码时忘记使用 catch 方法处理异常导致错误排查非常困难的问题。由于 Promise 的 try-catch 机制,异常可能会被内部消化掉。这种错误被内部消化的问题也被称为 unhandled rejection,从字面上看就是在 Rejected 时没有找到相应处理的意思。
function JSONPromise(value) {
return new Promise(function (resolve) {
resolve(JSON.parse(value));
});
}
// 运行示例
var string = '{}';
JSONPromise(string).then(function (object) {
conosle.log(object);
});
在这个例子里,我们错把 console 拼成了 conosle,因此会发生如下错误:
ReferenceError: conosle is not defined
不过在 chrome 中实测查找这种错误已经相当精准了。所以以前用 jQuery 的时候用过 done,后来在实际项目中并没有使用过 done 方法。
2)finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与 done 方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
(value) => P.resolve(callback()).then(() => value),
(reason) =>
P.resolve(callback()).then(() => {
throw reason;
})
);
};
这个还是很有用的,我们经常在 ajax 无论成功还是失败后都要关闭 loading。我一般使用这个库promise.prototype.finally。
Promise 只能进行异步操作?
var promise = new Promise(function (resolve) {
console.log(1); // 1
resolve(3);
});
promise.then(function (value) {
console.log(value); // 3
});
console.log(2); // 2
执行上面的代码,会依次输出 1,2,3。首先 new Promise 中的函数会立即执行,然后是外面的 console.log(2),最后是 then 回调中的函数。
由于 promise.then 执行的时候 promise 对象已经是确定状态,从程序上说对回调函数进行同步调用也是行得通的。
但是即使在调用 promise.then 注册回调函数的时候 promise 对象已经是确定的状态,Promise 也会以异步的方式调用该回调函数,这是在 Promise 设计上的规定方针。为什么要这样呢?
这涉及到同步调用和异步调用同时存在导致的混乱。
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==');
上面的代码如果在调用 onReady 之前 DOM 已经载入的话:对回调函数进行同步调用。
如果在调用 onReady 之前 DOM 还没有载入的话:通过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用。
因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的 log 消息顺序也会不同。
为了解决这个问题,我们可以选择统一使用异步调用的方式:
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==');
关于这个问题,在Effective JavaScript的第 67 项不要对异步回调函数进行同步调用中也有详细介绍:
- 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
- 如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。
- 对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。
- 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout 等异步 API。
为了避免上述中同时使用同步、异步调用可能引起的混乱问题,Promise 在规范上规定 Promise 只能使用异步调用方式。
由于 Promise 保证了每次调用都是以异步方式进行的,所以我们在实际编码中不需要调用 setTimeout 来自己实现异步调用:
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.resolve()章节的三个 promise,我们看看其执行顺序是怎样的?
var p1 = Promise.resolve(1);
var p2 = Promise.resolve(p1);
var p3 = new Promise(function (resolve, reject) {
resolve(p1);
});
var p4 = new Promise(function (resolve, reject) {
reject(p1);
});
p3.then(function (value) {
console.log('p3 : ' + value);
});
p2.then(function (value) {
console.log('p2 : ' + value);
});
p4.then(
function (value) {
console.log('p4-1 : ' + value);
},
function (value) {
console.log('p4-1 : ' + value);
}
);
p4.then(function (value) {
console.log('p4-2 : ' + value);
}).catch(function (value) {
console.log('p4-2 : ' + value);
});
p1.then(function (value) {
console.log('p1 : ' + value);
});
我们在比较新的浏览器控制台输出会发现顺序为 2,4-1,1,4-2,3(测试发现 chrome55、56 中则是最先打印出 3)。这个不知道怎么解释了,为什么 p3 会最后执行?暂时没找到什么可靠的资料,有大神知道的话,请评论指出。
Promise chain(Promise 方法链)
Promise chain 流程
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);
在上述代码中,我们没有为 then 方法指定第二个参数(onRejected),可以像下面这样来理解:
then:注册 onResolved 时的回调函数
catch:注册 onRejected 时的回调函数
1)taskA、taskB 都没有发生异常,会按照 taskA → taskB → finalTask 这个流程来进行处理
2)taskA 没有发生异常,taskB 发生异常,会按照 taskA → taskB → onRejected → finalTask 这个流程来进行处理
3)taskA 发生异常,会按照 taskA → onRejected → finalTask 这个流程来进行处理,TaskB 是不会被调用的
function taskA() {
console.log('Task A');
throw new Error('throw Error @ Task A');
}
function taskB() {
console.log('Task B'); // 不会被调用
}
function onRejected(error) {
console.log(error); // => 'throw Error @ Task A'
}
function finalTask() {
console.log('Final Task');
}
var promise = Promise.resolve();
promise.then(taskA).then(taskB).catch(onRejected).then(finalTask);
在本例中我们在 taskA 中使用了 throw 方法故意制造了一个异常。但在实际中想主动进行 onRejected 调用的时候,应该返回一个 Rejected 状态的 promise 对象。
promise chain 中如何传递参数?
如果 Task A 想给 Task B 传递一个参数该怎么办呢?其实非常简单,只要在 taskA 中 return 一个值,这个值会作为参数传递给 taskB。
function doubleUp(value) {
return value * 2;
}
function increment(value) {
return 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 chain
在使用 Promise 处理一些复杂逻辑的过程中,我们有时候会想要在发生某种错误后就停止执行 Promise 链后面所有的代码。
然而 Promise 本身并没有提供这样的功能,一个操作,要么成功,要么失败,要么跳转到 then 里,要么跳转到 catch 里。
具体怎么做,请查看这篇文章从如何停掉 Promise 链说起。
每次调用 then 都会返回一个新创建的 promise 对象
从代码上乍一看,aPromise.then(…).catch(…)像是针对最初的 aPromise 对象进行了一连串的方法链调用。
然而实际上不管是 then 还是 catch 方法调用,都返回了一个新的 promise 对象。
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
执行上面代码,证明了 then 和 catch 都返回了和调用者不同的 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
})
下面是一个由方法 1 中的 then 用法导致的比较容易出现的很有代表性的反模式的例子:
// then的错误使用方法
function badAsyncCall() {
var promise = Promise.resolve();
promise.then(function () {
// 任意处理
return newVar;
});
return promise;
}
这种写法有很多问题,首先在 promise.then 中产生的异常不会被外部捕获,此外,也不能得到 then 的返回值,即使其有返回值。
不仅 then 和 catch 都返回了和调用者不同的 promise 对象,Promise.all 和 Promise.race,他们都会接收一组 promise 对象为参数,并返回一个和接收参数不同的、新的 promise 对象。
使用 then 的第二个参数还是 catch 处理异常?
之前我们说过 .catch 也可以理解为 promise.then(undefined, onRejected)。那么使用这两种方法进行错误处理有什么区别呢?
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');
});
在上面的代码中,badMain 是一个不太好的实现方式(但也不是说它有多坏),goodMain 则是一个能非常好的进行错误处理的版本。
为什么说 badMain 不好呢?,因为虽然我们在.then 的第二个参数中指定了用来错误处理的函数,但实际上它却不能捕获第一个参数 onResolved 指定的函数(本例为 throwError)里面出现的错误。
也就是说,这时候即使 throwError 抛出了异常,onRejected 指定的函数也不会被调用(即不会输出”BAD”字样)。
与此相对的是,goodMain 的代码则遵循了 throwError → onRejected 的调用流程。这时候 throwError 中出现异常的话,在会被方法链中的下一个方法,即.catch 所捕获,进行相应的错误处理。
.then 方法中的 onRejected 参数所指定的回调函数,实际上针对的是其 promise 对象或者之前的 promise 对象,而不是针对.then 方法里面指定的第一个参数,即 onResolved 所指向的对象,这也是 then 和 catch 表现不同的原因。
1)使用 promise.then(onResolved, onRejected)的话
在 onResolved 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
2)在 promise.then(onResolved).catch(onRejected)的情况下
then 中产生的异常能在.catch 中捕获
3).then 和.catch 在本质上是没有区别的
需要分场合使用。
我们需要注意如果代码类似 badMain 那样的话,就可能出现程序不会按预期运行的情况,从而不能正确的进行错误处理。
IE8 及 IE8 以下 catch 兼容问题
IE8 及 IE8 以下即使已经引入了 Promise 的 polyfill,使用 catch 方法仍然会出现 identifier not found 的语法错误。
这是怎么回事呢?实际上这和 catch 是 ECMAScript 的保留字(Reserved Word)有关。
在 ECMAScript 3 中保留字是不能作为对象的属性名使用的。而 IE8 及以下版本都是基于 ECMAScript 3 实现的,因此不能将 catch 作为属性来使用,也就不能编写类似 promise.catch()的代码,因此就出现了 identifier not found 这种语法错误了。
而现代浏览器都支持 ECMAScript 5,而在 ECMAScript 5 中保留字都属于 IdentifierName,也可以作为属性名使用了。
点标记法(dot notation)要求对象的属性必须是有效的标识符(在 ECMAScript 3 中则不能使用保留字)。
但是使用中括号标记法(bracket notation)的话,则可以将非合法标识符作为对象的属性名使用。
var promise = Promise.reject(new Error('message'));
promise['catch'](function (error) {
console.error(error);
});
由于 catch 标识符可能会导致问题出现,因此一些类库(Library)也采用了 caught 作为函数名,而函数要完成的工作是一样的。
而且很多压缩工具自带了将 promise.catch 转换为 promise[‘catch’]的功能,所以可能不经意之间也能帮我们解决这个问题。
使用 reject 而不是 throw
var promise = new Promise(function (resolve, reject) {
throw new Error('message');
});
promise.catch(function (error) {
console.error(error); // => "message"
});
上面代码其实并没有什么问题,但是有两个不好的地方:
首先是因为我们很难区分 throw 是我们主动抛出来的,还是因为真正的其它异常导致的。
其次本来这是和调试没有关系的地方,throw 时就会触发调试器的 break 行为,会干扰浏览器的调试器中 break 的功能的正常使用。
所以使用 reject 会比使用 throw 安全。
再议 Promise.resolve 和 Thenable
之前我们已经讲过 Promise.resolve 能将 thenable 对象转化为 promise 对象。接下来我们再看看将 thenable 对象转换为 promise 对象这个功能都能具体做些什么事情。
以 Web Notification 为例,普通使用回调函数方式如下:
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"));
}
}
// 运行实例
// 第二个参数是传给 `Notification` 的option对象
notifyMessage('Hi!', {}, function (error, notification) {
if (error) {
return console.error(error);
}
console.log(notification); // 通知对象
});
使用 Promise 改写回调:
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"));
}
}
function notifyMessageAsPromise(message, options) {
return new Promise(function (resolve, reject) {
notifyMessage(message, options, function (error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
});
}
// 运行示例
notifyMessageAsPromise('Hi!')
.then(function (notification) {
console.log(notification); // 通知对象
})
.catch(function (error) {
console.error(error);
});
使用 thenable 对象形式:
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(message, options) {
return {
then: function (resolve, reject) {
notifyMessage(message, options, function (error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
},
};
}
// 运行示例
Promise.resolve(notifyMessageAsThenable('message'))
.then(function (notification) {
console.log(notification); // 通知对象
})
.catch(function (error) {
console.error(error);
});
Thenable 风格表现为位于回调和 Promise 风格中间的一种状态,不用考虑 Promise 的兼容问题。一般不作为类库的公开 API,更多情况下是在内部使用 Thenable。Thenable 对象更多的是用来在 Promise 类库之间进行相互转换。
使用 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 () {
// Q promise对象可以使用finally方法
console.log('finally');
});
Deferred 和 Promise
Deferred 和 Promise 不同,它没有共通的规范,每个 Library 都是根据自己的喜好来实现的。
在这里,我们打算以 jQuery.Deferred 类似的实现为中心进行介绍。
简单来说,Deferred 和 Promise 具有如下的关系。
- Deferred 拥有 Promis(当然也有的 Deferred 实现并没有内涵 Promise)
- Deferred 具备对 Promise 的状态进行操作的特权方法
用 Deferred 实现的 getURL(Deferred 基于 promise 实现):
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(this.promise, reason);
};
function getURL(URL) {
var deferred = new Deferred();
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
deferred.resolve(req.responseText);
} else {
deferred.reject(new Error(req.statusText));
}
};
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 实现的 getURL:
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 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 进行操作的特权方法,以及可以对流程控制进行自由定制。
上面我们只是简单的实现了一个 Deferred ,我想你已经看到了它和 Promise 之间的差异了吧。
如果说 Promise 是用来对值进行抽象的话,Deferred 则是对处理还没有结束的状态或操作进行抽象化的对象,我们也可以从这一层的区别来理解一下这两者之间的差异。
换句话说,Promise 代表了一个对象,这个对象的状态现在还不确定,但是未来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而 Deferred 对象表示了一个处理还没有结束的这种事实,在它的处理结束的时候,可以通过 Promise 来取得处理结果。
使用 Promise.race 和 delay 取消 XHR 请求
XHR 有一个 timeout 属性,使用该属性也可以简单实现超时功能,但是为了能支持多个 XHR 同时超时或者其他功能,我们采用了容易理解的异步方式在 XHR 中通过超时来实现取消正在进行中的操作。
1)让 Promise 等待指定时间
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delayPromise(100).then(function () {
alert('已经过了100ms!');
});
- 使用 promise.race()来实现超时 promise:
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
throw new Error('Operation timed out after ' + ms + ' ms');
});
return Promise.race([promise, timeout]);
}
上面代码 promise 的状态改变的时间超过了 ms 就会 throw Error。
// 运行示例
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);
});
3)定制 Error 对象
为了能区分这个 Error 对象的类型,我们再来定义一个 Error 对象的子类 TimeoutError。
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;
它的使用方法和普通的 Error 对象一样,使用 throw 语句即可
var promise = new Promise(function () {
throw new TimeoutError('timeout');
});
promise.catch(function (error) {
console.log(error instanceof TimeoutError); // true
});
有了这个 TimeoutError 对象,我们就能很容易区分捕获的到底是因为超时而导致的错误,还是其他原因导致的 Error 对象了。
4)通过超时取消 XHR 操作
取消 XHR 操作本身的话并不难,只需要调用 XMLHttpRequest 对象的 abort()方法就可以了。
为了能在外部调用 abort()方法,我们先对之前本节出现的 getURL 进行简单的扩展,cancelableXHR 方法除了返回一个包装了 XHR 的 promise 对象之外,还返回了一个用于取消该 XHR 请求的 abort 方法。
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;
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function timeoutPromise(promise, ms) {
var timeout = delayPromise(ms).then(function () {
return Promise.reject(
new TimeoutError('Operation timed out after ' + ms + ' ms')
);
});
return Promise.race([promise, timeout]);
}
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,
};
}
var object = cancelableXHR('http://httpbin.org/get');
// main
timeoutPromise(object.promise, 1000)
.then(function (contents) {
console.log('Contents', contents);
})
.catch(function (error) {
if (error instanceof TimeoutError) {
object.abort();
return console.log(error);
}
console.log('XHR Error :', error);
});
5)代码分割优化处理
在前面的 cancelableXHR 中,promise 对象及其操作方法都是在一个对象中返回的,看起来稍微有些不太好理解。
从代码组织的角度来说一个函数只返回一个值(promise 对象)是一个非常好的习惯,但是由于在外面不能访问 cancelableXHR 方法中创建的 req 变量,所以我们需要编写一个专门的函数(上面的例子中的 abort)来对这些内部对象进行处理。
当然也可以考虑到对返回的 promise 对象进行扩展,使其支持 abort 方法,但是由于 promise 对象是对值进行抽象化的对象,如果不加限制的增加操作用的方法的话,会使整体变得非常复杂。
大家都知道一个函数做太多的工作都不认为是一个好的习惯,因此我们不会让一个函数完成所有功能,也许像下面这样对函数进行分割是一个不错的选择。
- 返回包含 XHR 的 promise 对象
- 接收 promise 对象作为参数并取消该对象中的 XHR 请求
将这些处理整理为一个模块的话,以后扩展起来也方便,一个函数所做的工作也会比较精炼,代码也会更容易阅读和维护。
使用 common.js 规范来写 cancelableXHR.js:
'use strict';
var requestMap = {};
function createXHRPromise(URL) {
var req = new XMLHttpRequest();
var promise = new Promise(function (resolve, reject) {
req.open('GET', URL, true);
req.onreadystatechange = function () {
if (req.readyState === XMLHttpRequest.DONE) {
delete requestMap[URL];
}
};
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 req'));
};
req.send();
});
requestMap[URL] = {
promise: promise,
request: req,
};
return promise;
}
function abortPromise(promise) {
if (typeof promise === 'undefined') {
return;
}
var request;
Object.keys(requestMap).some(function (URL) {
if (requestMap[URL].promise === promise) {
request = requestMap[URL].request;
return true;
}
});
if (request != null && request.readyState !== XMLHttpRequest.UNSENT) {
request.abort();
}
}
module.exports.createXHRPromise = createXHRPromise;
module.exports.abortPromise = abortPromise;
调用:
var cancelableXHR = require('./cancelableXHR');
var xhrPromise = cancelableXHR.createXHRPromise('http://httpbin.org/get'); // 创建包装了XHR的promise对象
xhrPromise.catch(function (error) {
// 调用 abort 抛出的错误
});
cancelableXHR.abortPromise(xhrPromise); // 取消在创建的promise对象的请求操作
promise 串行处理
Promise.all()可以进行 promise 对象的并行处理,那么怎么实现串行处理呢?
我们将处理内容统一放到数组里,再配合 for 循环进行处理:
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, []);
// 返回promise对象的函数的数组
var tasks = [request.comment, request.people];
var promise = Promise.resolve();
// 开始的地方
for (var i = 0; i < tasks.length; i++) {
var task = tasks[i];
promise = promise.then(task).then(pushValue);
}
return promise;
}
// 运行示例
main()
.then(function (value) {
console.log(value);
})
.catch(function (error) {
console.error(error);
});
上面代码中的 promise = promise.then(task).then(pushValue)通过不断对 promise 进行处理,不断的覆盖 promise 变量的值,以达到对 promise 对象的累积处理效果。
但是这种方法需要 promise 这个临时变量,从代码质量上来说显得不那么简洁。我们可以使用 Array.prototype.reduce 来优化 main 函数:
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
var tasks = [request.comment, request.people];
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}
实际上我们可以提炼出进行顺序处理的函数:
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}
这样我们只要如下调用,代码也更加清晰易懂了:
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 sequenceTasks([request.comment, request.people]);
}
// 运行示例
main()
.then(function (value) {
console.log(value);
})
.catch(function (error) {
console.error(error);
});
同时请求按序处理
下面的内容来自 google 开发社区的一篇关于 promise 的文章JavaScript Promise:简介
假设我们要根据 story.json 通过 ajax 获取章节内容,每一次 ajax 只能获取一节内容。那么怎么做到又快又能按序展示章节内容呢?即如果第一章下载完后,我们可将其添加到页面。这可让用户在其他章节下载完毕前先开始阅读。如果第三章比第二章先下载完后,我们不将其添加到页面,因为还缺少第二章。第二章下载完后,我们可添加第二章和第三章,后面章节也是如此添加。
前一节的串行方法只能一个 ajax 请求 task 处理完后再去执行下一个 task,而 Promise.all()能同时请求,但是只有全部请求结束后才能得到有序的数组。
具体实现请看下面实例。
我们可以使用 JSON 来同时获取所有章节,然后创建一个向文档中添加章节的顺序。
story.json 如下:
{
"heading": "<h1>A story about something</h1>",
"chapterUrls": [
"chapter-1.json",
"chapter-2.json",
"chapter-3.json",
"chapter-4.json",
"chapter-5.json"
]
}
具体处理代码:
function getJson(url) {
return get(url).then(JSON.parse)
}
getJSON('story.json')
.then(function (story) {
addHtmlToPage(story.heading) // 文章头部添加到页面
// 将拿到的chapterUrls数组map为json promises数组,这样可以保证并行下载
return story.chapterUrls
.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// 用reduce方法链式调用promises,并将每个章节的内容到添加页面
return sequence.then(function () {
// 等待获取当前准备插入页面的顺序的资源,然后等待这个顺序对应章节的成功请求
// Wait for everything in the sequence so far, then wait for this chapter to arrive.
return chapterPromise
}).then(function(chapter) {
addHtmlToPage(chapter.html) // 将章节内容到添加页面
})
}, Promise.resolve())
})
.then(function() {
addTextToPage('All done') // 页面添加All done文字
})
.catch(function(err) {
// catch错误信息
addTextToPage('Argh, broken: '' + err.message)
})
.then(function() {
document.querySelector('.spinner').style.display = 'none' // 关闭加载提示
})
Promise 和链式调用
在 Promise 中你可以将 then 和 catch 等方法连在一起写。这非常像 DOM 或者 jQuery 中的链式调用。
一般的方法链都通过返回 this 将多个方法串联起来。
那么怎么在不改变已有采用了方法链编写的代码的外部接口的前提下,如何在内部使用 Promise 进行重写呢?
1)fs 中的方法链
以 Node.js 中的 fs 为例。
此外,这里的例子我们更重视代码的易理解性,因此从实际上来说这个例子可能并不算太实用。
有 fs-method-chain.js:
'use strict';
var fs = require('fs');
function File() {
this.lastValue = null;
}
// Static method for File.prototype.read
File.read = function FileRead(filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.read = function (filePath) {
this.lastValue = fs.readFileSync(filePath, 'utf-8');
return this;
};
File.prototype.transform = function (fn) {
this.lastValue = fn.call(this, this.lastValue);
return this;
};
File.prototype.write = function (filePath) {
this.lastValue = fs.writeFileSync(filePath, this.lastValue);
return this;
};
module.exports = File;
调用:
var File = require('./fs-method-chain');
var inputFilePath = 'input.txt',
outputFilePath = 'output.txt';
File.read(inputFilePath)
.transform(function (content) {
return '>>' + content;
})
.write(outputFilePath);
2)基于 Promise 的 fs 方法链
下面我们就在不改变刚才的方法链对外接口的前提下,采用 Promise 对内部实现进行重写。
'use strict';
var fs = require('fs');
function File() {
this.promise = Promise.resolve();
}
// Static method for File.prototype.read
File.read = function (filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
File.prototype['catch'] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
File.prototype.read = function (filePath) {
return this.then(function () {
return fs.readFileSync(filePath, 'utf-8');
});
};
File.prototype.transform = function (fn) {
return this.then(fn);
};
File.prototype.write = function (filePath) {
return this.then(function (data) {
return fs.writeFileSync(filePath, data);
});
};
module.exports = File;
3)两者的区别
要说 fs-method-chain.js 和 Promise 版两者之间的差别,最大的不同那就要算是同步和异步了。
如果在类似 fs-method-chain.js 的方法链中加入队列等处理的话,就可以实现几乎和异步方法链同样的功能,但是实现将会变得非常复杂,所以我们选择了简单的同步方法链。
Promise 版的话如同之前章节所说只会进行异步操作,因此使用了 promise 的方法链也是异步的。
另外两者的错误处理方式也是不一致的。
虽然 fs-method-chain.js 里面并不包含错误处理的逻辑,但是由于是同步操作,因此可以将整段代码用 try-catch 包起来。
在 Promise 版提供了指向内部 promise 对象的 then 和 catch 别名,所以我们可以像其它 promise 对象一样使用 catch 来进行错误处理。
如果你想在 fs-method-chain.js 中自己实现异步处理的话,错误处理可能会成为比较大的问题;可以说在进行异步处理的时候,还是使用 Promise 实现起来比较简单。
4)Promise 之外的异步处理
如果你很熟悉 Node.js 的話,那么看到方法链的话,你是不是会想起来 Stream 呢。
如果使用 Stream 的话,就可以免去了保存 this.lastValue 的麻烦,还能改善处理大文件时候的性能。 另外,使用 Stream 的话可能会比使用 Promise 在处理速度上会快些。
因此,在异步处理的时候并不是说 Promise 永远都是最好的选择,要根据自己的目的和实际情况选择合适的实现方式。
5)Promise wrapper
再回到 fs-method-chain.js 和 Promise 版,这两种方法相比较内部实现也非常相近,让人觉得是不是同步版本的代码可以直接就当做异步方式来使用呢?
由于 JavaScript 可以向对象动态添加方法,所以从理论上来说应该可以从非 Promise 版自动生成 Promise 版的代码。(当然静态定义的实现方式容易处理)
尽管 ES6 Promises 并没有提供此功能,但是著名的第三方 Promise 实现类库 bluebird 等提供了被称为 Promisification 的功能。
如果使用类似这样的类库,那么就可以动态给对象增加 promise 版的方法。
var fs = Promise.promisifyAll(require('fs'));
fs.readFileAsync('myfile.js', 'utf8')
.then(function (contents) {
console.log(contents);
})
.catch(function (e) {
console.error(e.stack);
});
前面的 Promisification 具体都干了些什么光凭想象恐怕不太容易理解,我们可以通过给原生 Array 增加 Promise 版的方法为例来进行说明。
在 JavaScript 中原生 DOM 或 String 等也提供了很多创建方法链的功能。Array 中就有诸如 map 和 filter 等方法,这些方法会返回一个数组类型,可以用这些方法方便的组建方法链。
'use strict';
function ArrayAsPromise(array) {
this.array = array;
this.promise = Promise.resolve();
}
ArrayAsPromise.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
ArrayAsPromise.prototype['catch'] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
Object.getOwnPropertyNames(Array.prototype).forEach(function (methodName) {
// Don't overwrite
if (typeof ArrayAsPromise[methodName] !== 'undefined') {
return;
}
var arrayMethod = Array.prototype[methodName];
if (typeof arrayMethod !== 'function') {
return;
}
ArrayAsPromise.prototype[methodName] = function () {
var that = this;
var args = arguments;
this.promise = this.promise.then(function () {
that.array = Array.prototype[methodName].apply(that.array, args);
return that.array;
});
return this;
};
});
module.exports = ArrayAsPromise;
module.exports.array = function newArrayAsPromise(array) {
return new ArrayAsPromise(array);
};
原生的 Array 和 ArrayAsPromise 在使用时有什么差异呢?我们可以通过对上面的代码进行测试来了解它们之间的不同点。
'use strict';
var assert = require('power-assert');
var ArrayAsPromise = require('../src/promise-chain/array-promise-chain');
describe('array-promise-chain', function () {
function isEven(value) {
return value % 2 === 0;
}
function double(value) {
return value * 2;
}
beforeEach(function () {
this.array = [1, 2, 3, 4, 5];
});
describe('Native array', function () {
it('can method chain', function () {
var result = this.array.filter(isEven).map(double);
assert.deepEqual(result, [4, 8]);
});
});
describe('ArrayAsPromise', function () {
it('can promise chain', function (done) {
var array = new ArrayAsPromise(this.array);
array
.filter(isEven)
.map(double)
.then(function (value) {
assert.deepEqual(value, [4, 8]);
})
.then(done, done);
});
});
});
我们看到,在 ArrayAsPromise 中也能使用 Array 的方法。原生的 Array 是同步处理,而 ArrayAsPromise 则是异步处理。
仔细看一下 ArrayAsPromise 的实现,也许你已经注意到了,Array.prototype 的所有方法都被实现了。但是,Array.prototype 中也存在着类似 array.indexOf 等并不会返回数组类型数据的方法,这些方法如果也要支持链式调用的话就有些不自然了。
在这里非常重要的一点是,我们可以通过这种方式,为具有接收相同类型数据接口的 API 动态的创建 Promise 版的 API。如果我们能意识到这种 API 的规则性的话,那么就可能发现一些新的使用方法。
自己实现一个 Promise 类
剖析 Promise 内部结构,一步一步实现一个完整的、能通过所有 Test case 的 Promise 类
Promise 反面模式(anti-pattern)
关于反面模式,维基百科是这样定义的:在软件工程中,一个反面模式(anti-pattern 或 antipattern)指的是在实践中明显出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。
Promise 中常见的反面模式有嵌套的 promise、没有正确 error handle 等。
Promise 常见错误
We have a problem with promises 原文
We have a problem with promises 中文翻译
其他强大的异步处理方式
1)使用 async/await
async/await 更加强大,能写出更像同步的代码。但是基础仍然是要掌握 Promise。
2)使用 Rxjs(Angular2 后框架自带)。