Promise小书(长文)

前言

本文主要参考了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简介

目前主要有三种类型

  1. 构造函数(Constructor)

创建一个promise实例:

var promise = new Promise(function (resolve, reject) {
	// 异步处理
    // 处理结束后、调用resolve 或 reject
})
  1. 实例方法(Instance Method)

    promise.then(onFulfilled, onRejected)

    promise.catch(onRejected)

  2. 静态方法(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

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!')
})
  1. 使用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反面模式原文

Promise反面模式中文翻译

Promise常见错误

We have a problem with promises原文

We have a problem with promises中文翻译

其他强大的异步处理方式

1)使用async/await

async/await更加强大,能写出更像同步的代码。但是基础仍然是要掌握Promise。

2)使用Rxjs(Angular2后框架自带)。

参考

JavaScript Promise迷你书(中文版)

阮一峰 ECMAScript6入门 Promise对象

JavaScript Promise:简介

posted @ 2017-09-20 22:21  万里秋山  阅读(1021)  评论(3编辑  收藏  举报