前言
所谓promise就是一个用来传递异步操作的消息,他代表了某个未来才能知道事件的结果,我们用这个代表了未来的结果先一步决定其下一步进行的操作,在ES6标准出之前我们是用笨拙的回调函数进行异步操作。
正文
事实上promise出现的意义要大得多,表面看来它只是让我们逃离了可怕的回调金字塔,让我们的代码颜值更高,语义化更强,更易于测试巴拉巴拉的一堆,但一个很多语言都坚定采用的标准有这么简单嘛?
promise与callback
至少在js里没这么简单,在我们使用笨拙的回调的时代,它实际上首先剥夺了我们正常的调用栈,作用域是混乱的,我们无法找到上层的作用域,实质上在promise中基本由promise对象和其原型链内置方法保存了上层作用域状态并传递下来,另外回调函数没有让我们使用catch、throw等差错关键字的机制,因为异步操作与回调函数之间的联系是只有参数传递的,我们要检错一般必须显式根据回调函数参数等判断错误报错。
总体看来,可以近似理解为,将回调函数看做高度封装的低耦合高内聚函数,其跟外界的联系只有参数一种途径,这样设定或许可以避免很多外界影响,但同时也带来一系列不方便,我认为这才是promise引入的意义,其使用上确实相对于回调函数可能多了一些步骤,但回调函数的优点它全部拥有,缺点却被内部实现机制弥补了,其存在缺点可能就是形式上有一些繁琐,promise开始就无法结束,promise的pending无法获知结果等,这在以后将要发行的async\await的提案中得到了很大程度上的解决,我们在此就不做展开了,下面开始介绍promise特性,以及自己实现promise的构想。
promise API
先给出一个经典promise,ajax的promise实现
var getJson = function(url){
var promise = new Promise(function (resolve, reject){
var client = new XMLHttpRequest();
client.open('GET', url)
client.onreadystatechange = handler
client.responseType = 'josn'
client.setRequestHeader('Accept', 'application/json')
client.send()
function handler(){
if (this.readyState !== 4) {
return
}else{
reject(new Error(this.statusText))
}
}
})
return promise
}
getJson('/posts.json').then(function(json){
//coding
cosole.log('Content:' + json)
},function(error){
console.log('出错了', error)
})
上面的代码首先封装了ajax请求json的xhr对象,返回了promise后再链式调用,如果成功了就调用第一个参数,失败了则调用第二个
promise对象比较特殊,他贯穿未来与现在,一旦创建就不受外界影响,只能等他自己变化状态。总共有三种状态,pending(进行中)这状态也就是结果还没出来,通常我们都会忽略这个状态,只关注它的结果,当然这个状态也很难观测
结果有两种状态,resolved(已完成)和rejected(已失败),给出一个异步加载图片的例子。
function loadImageAsync(url){
return new Promise(function(resolve, reject){
var image = new Image()
image.onload = () => {
resolve(image)
}
image.onerror = () => {
reject(new Error('could not load image at' + url))
}
image.src = url
})
}
resolve和reject函数接受参数,并将参数传给链式函数后then中的两个函数作为参数,此参数可以是数值、对象,但也可能是一个新的异步操作,如resolve( new Promise(function(){…})),reject则大多传递一个error对象实例,里面储存着异步操作失败的一些信息。
接下来就可以用then函数继续操作了,then函数中接受两个函数作为参数,分别为状态变化为resolved和rejected(可选)才运行的函数,与此同时then方法进行完毕后会返回一个新的promise实例,所以then大多采用链式写法。
resolve和reject函数接受参数,并将参数传给then中的函数作为参数,此参数可以是数值、对象,但也可能是一个新的异步操作,如resolve( new Promise(function(){…})),reject则大多传递一个error对象实例,里面储存着异步操作失败的一些信息
另外还有catch方法,实质上是.then(null, rejection),用于指定发生错误时的回调函数,下面给出一个例子
getJson('./post.json')
.then((post) => {
//这里是resolved状态进行的代码
})
.catch((error) => {
console.log(error.info)
})
如果对象状态变为了resolved那么就进行then中函数,否则进行catch中函数,因为promise中的错误具有冒泡的性质,会一直向后传递,直到错误被捕获为止,所以一般then都只写一个参数,错误交给catch处理,另外catch所返回的仍然是一个promise对象。
另外还有一些API是Promise.all()和Promise.race()以及一些其他方法就不详细叙述了,前两者是用于将多个promise实例包装成一个新的promise实例,有兴趣的朋友可以自己查阅资料。
要注意的是,promise构造函数接受一个函数作为参数,函数的参数有两个resolve和reject,分别可以将promise的状态相应改变。
promise ployfill
下面说一下实现的构想,主要还是理解思路,首先先明确promise的几个关键点,一个就是状态变量,一个是链式then方法,一个在高层作用域,可以被有限的几个方法更改但不能被其他作用影响,功能说清楚后,其实如何设定一目了然,闭包完全符合以上特征,当然具体如何使用还是需要斟酌,另外链式调用就不用多说了,返回的是promise实例,实例进行then方法处理,存在实例上的方法,就是放在原型链上的喽。占坑,考虑具体实现
有兴趣的可以先看promise实现深度剖析
总结
promise解决了什么问题
- 调用栈混乱,使用then链式API传递参数和结果
- 安全性问题,promise决议后不可变性,使得结果不会外部篡改
- 合理有序的流程控制以及检错机制
promise的局限性
- promise链中的错误难以捕捉,外部难以介入观察错误,catch方法又比较局限,且无finally方法
- promise决议值参数只有一个并且只能被决议一次,如果遇到复杂业务场景还需要进行其他逻辑处理
- promise有一定侵入性,首先从回调迁移到promise有一定编码习惯和重构成本,要将所有函数promise化,其次使用了promise就必须全部使用promise,最后全部promise会将所有行为(包括同步行为)都转换为异步行为
- promise一旦创建无法取消,暂时没有特别好的黑科技,究其原因还是promise暂时只提供了控制实例以及与相邻实例通信的功能,而promise链暂时是无法完全掌控的。