本文主要参考了JavaScript Promise迷你书,连接在文末与其余参考一块儿列出。html
Promise是异步编程的一种解决方案。ES6 Promise的规范来源于Promises/A+社区,它有不少版本的实现。git
Promise比传统的解决方案(回调函数和事件)更合理和更强大,能够避免回调地狱。使用Promise来统一处理异步操做,更具语义化、易于理解、有利维护。es6
Promise接口的基本思想是让异步操做返回一个Promise对象,咱们能够对这个对象进行一些操做。github
Promise对象只有三种状态。web
这三种的状态的变化途径只有两种。ajax
这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。所以,Promise对象的最终结果只有两种。chrome
异步操做成功,Promise对象传回一个值,状态变为resolved。npm
异步操做失败,Promise对象抛出一个错误,状态变为rejected。编程
目前主要有三种类型json
1) 构造函数(Constructor)
建立一个promise实例:
var promise = new Promise(function (resolve, reject) { // 异步处理 // 处理结束后、调用resolve 或 reject })
2) 实例方法(Instance Method)
promise.then(onFulfilled, onRejected) promise.catch(onRejected)
3) 静态方法(Static Method)
Promise.all()、 Promise.race()、Promise.resolve()、Promise.reject()
给Promise构造函数传递一个函数fn做为参数实例化便可。这个函数fn有两个参数(resolve和reject),在fn中指定异步等处理:
// 建立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对象添加处理方法主要有如下两种:
被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)
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(error)是和Promise.resolve(value)相似的静态方法,是new Promise()方法的快捷方式。
好比Promise.reject(new Error('出错了'))就是下面代码的语法糖形式:
new Promise(function (resolve, reject) { reject(new Error('出错了')) })
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.all([p1, p2, p3])
上面代码中,Promise.all方法接受一个数组做为参数,p一、p二、p3都是Promise实例,若是不是,就会先调用Promise.resolve方法,将参数转为Promise实例,再进一步处理。
p的状态由p一、p二、p3决定,分红两种状况。
(1)只有p一、p二、p3的状态都变成resolved,p的状态才会变成resolved,此时p一、p二、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p一、p二、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的时间。
var p = Promise.race([p1, p2, p3])
与Promise.all相似,可是只要p一、p二、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进行取消的操做。
因为不少浏览器不支持ES6 Promises,咱们须要一些第三方实现的和Promise兼容的类库。
选择Promise类库首先要考虑的是否具备Promises/A+兼容性。
Promises/A+是ES6 Promises的前身,Promise的then也是由社区的规范而来。
这些类库主要有两种:Polyfill和扩展类库
1)Polyfill
2)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有如下两个特色。
那么它是如何将异常抛到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。
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项不要对异步回调函数进行同步调用中也有详细介绍:
为了不上述中同时使用同步、异步调用可能引发的混乱问题,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(测试发现chrome5五、56中则是最早打印出3)。这个不知道怎么解释了,为何p3会最后执行?暂时没找到什么可靠的资料,有大神知道的话,请评论指出。
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对象。
若是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处理一些复杂逻辑的过程当中,咱们有时候会想要在发生某种错误后就中止执行Promise链后面全部的代码。
然而Promise自己并无提供这样的功能,一个操做,要么成功,要么失败,要么跳转到then里,要么跳转到catch里。
具体怎么作,请查看这篇文章从如何停掉 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对象。
以前咱们说过 .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如下即便已经引入了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']的功能,因此可能不经意之间也能帮咱们解决这个问题。
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对象。接下来咱们再看看将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不一样,它没有共通的规范,每一个Library都是根据本身的喜爱来实现的。
在这里,咱们打算以jQuery.Deferred相似的实现为中心进行介绍。
简单来讲,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,因此大致的流程仍是差很少的,不过Deferred有对Promise进行操做的特权方法,以及能够对流程控制进行自由定制。
上面咱们只是简单的实现了一个Deferred ,我想你已经看到了它和Promise之间的差别了吧。
若是说Promise是用来对值进行抽象的话,Deferred则是对处理尚未结束的状态或操做进行抽象化的对象,咱们也能够从这一层的区别来理解一下这二者之间的差别。
换句话说,Promise表明了一个对象,这个对象的状态如今还不肯定,可是将来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而Deferred对象表示了一个处理尚未结束的这种事实,在它的处理结束的时候,能够经过Promise来取得处理结果。
XHR有一个timeout属性,使用该属性也能够简单实现超时功能,可是为了能支持多个XHR同时超时或者其余功能,咱们采用了容易理解的异步方式在XHR中经过超时来实现取消正在进行中的操做。
1)让Promise等待指定时间
function delayPromise (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms) }) } delayPromise(100).then(function () { alert('已通过了100ms!') })
2) 使用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对象是对值进行抽象化的对象,若是不加限制的增长操做用的方法的话,会使总体变得很是复杂。
你们都知道一个函数作太多的工做都不认为是一个好的习惯,所以咱们不会让一个函数完成全部功能,也许像下面这样对函数进行分割是一个不错的选择。
将这些处理整理为一个模块的话,之后扩展起来也方便,一个函数所作的工做也会比较精炼,代码也会更容易阅读和维护。
使用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.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中你能够将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内部结构,一步一步实现一个完整的、能经过全部Test case的Promise类
关于反面模式,维基百科是这样定义的:在软件工程中,一个反面模式(anti-pattern或antipattern)指的是在实践中明显出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。
Promise中常见的反面模式有嵌套的promise、没有正确error handle等。
We have a problem with promises原文
We have a problem with promises中文翻译
1)使用async/await
async/await更增强大,能写出更像同步的代码。可是基础仍然是要掌握Promise。
2)使用Rxjs(Angular2后框架自带)。