ES2015的promise已经出来很久,在工作中也经常使用,但未曾深究其原理,更不用谈自己实现了,直至上次和同事交流promise如何实现才开始思考这个问题,毕竟使用方法很简单,人人都会,但是如果都是只用而不知道它的工作原理,那以后使用的时候出现问题或者这个对象发生改变时又如何应对呢?
前言
清风拂山岗 明月照大江
一般来说,只知其然可能是一种最快的学习方法,因为能快速使用,但现在看来,如果你知其所以然,其实才是最小学习成本,因为这代表你已经掌握它的核心,所以未来它如何改变,你都会知道其本质。
个人认为,要实实在在的掌握一个东西,可通过WWH原则(what why how)进行深入。
WHAT 什么是Promise?
Promise是ES6提出的一种异步解决方案,翻译过来就是“承诺”的意思,我们可以想象成向某某承诺做某某事,如果做成功了,就返回成功结果;如果失败了,就返回失败的原因。这样下来我们才知道这件事情做没有做,失败的原因是什么,并且promise一旦结束,就不可以改变结果。1
promise.then(onFulfilled, onRejected)
就像答应女票放假带她出去玩,如果去玩了,可以unlock新姿势;相应的,如果没有去,那就可以做好睡沙发的准备 ;-)
Promise规范
规范 TL;DR 直接看重点
- 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
- 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
- promise必须实现then方法,且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
- then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。
该规范主要规定了promise的状态和then方法,通过它我们知道什么是promise,以及它各个方法的作用。
就像OO思维中说到的鸭子原则,如果有一种动物走起路来像鸭子,叫起来也是“呱呱呱”,那么它就是鸭子。
浏览器中的promise
可以看到,chrome下有finally、race等方法并没有在规范中出现,这是由于各浏览器or第三方库自己实现的程度不同导致的,我们只需记住规范中规定的就可以。
Promise常规用法
1 | function someAsync(){ |
正文
WHY 为什么要用Promise?
前面也提到了,Promise是一种异步解决方案,而它的提出也是为了解决异步编程的嵌套问题。
可以看看这段代码中的回调 滴滴出行动画
还好屏幕够宽,能够看到完整的缩进,要是被自动换行…可啪。
HOW 如何实现 如何使用?
前面说了这么多理论的东西,目的是在实现自定义promise的时候心里有数
根据之前的Promise/A+规范,我们现在开始动手自己实现Promise,先放实现功能list和代码,代码中会给对应功能的序号,下面会给出详细。步骤目录:
- thenable实现
- resolve回调实现
- reject回调实现
- 状态处理
- 非异步处理
- 链式调用
实现代码:
1 |
|
- then实现
promise提供一个then方法,在then方法里传入成功or失败的回调函数,在开始使用的时候以为then是在异步完成后执行的,后来发现其只是一个注册方法,只负责将传入的回调函数注册到promise中供resolve使用。
可以看到,then方法将我们传入的成功or失败回调函数存到对应的数组中,这样一来,当异步完成后我们根据结果遍历对应的数组就可以了。then里面的函数和之前的回调函数一样,能够在异步操作执行完成后被执行。
简单来看,promise通过then方法将原来的回调函数分离出来,实现链式调用的目的。这点像1.5.3之前的$.ajax写法,通过内部声明success来执行回调,[email protected]后有自己的一套promise实现ajax链式调用,感兴趣可自行了解,这里不详谈。 - resolve回调实现
resolve表示promise的状态进入到”fullfiled”,异步成功情况下的回调函数。
其原理是在异步操作完成后,将then注册的回调函数们依次取出执行。 - reject回调实现
reject表示promise的状态进入到”rejected”,异步失败情况下的回调函数,原理同上。 - 状态处理
规范中第一点就谈到了promise的状态,这里需要补充的就是其状态变换过程只有2种:
- pending -> fulfilled
- pending -> rejected
- 非异步处理
如果省略setTimeout这一步,你会发现当实例化diyPromise时传入的是同步函数时,then注册的回调会失效。
这是因为当传入同步函数时,resolve/reject方法会在实例化时就执行,而此时还未执行then方法,所以resolve时遍历了一个空的数组,从而导致回调失效。
所以加上setTimeout(fn,0)的目的是为了防止resolve/reject在then方法之前被调用执行。 - 链式调用
通过返回自身达到链式调用的目的,也就是多次使用then注册多个回调方法。
下面我们来用用刚刚创建好的diyPromise1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function someAsync(){
return new diyPromise(function(resolve,reject){
setTimeout(()=>{
if(Math.random()>0.5){
resolve("大于0.5");
}else{
reject("小于0.5");
}
},1000)
})
}
someAsync().then(res=>{
console.log('done: ' + res);
}, rej=>{
console.log('fail: ' + rej);
}).then(res=>{
console.log("onemoretime: " + res);
})
//done: 大于0.5
//onemoretime: 大于0.5
可以看到,我们使用了两个then注册了2个resolve回调,结果也是依次显示了出来。
结语
写这篇文章最初的目的也是想深入理解promise,对于我来说最好的方式就是自己动手造一个轮子。
在ES6之前,我们想要使用Promise,一般会借助于第三方库如bluebird.js,知道原理后就可以自己手写了。
随着各个浏览器紧跟ECMA的步伐,我们也能正常使用原生Promisre对象。
不同厂家实现Promise的程度不同,也添加了不同的特性如race、finally等,在开发中还是在遵循标准的情况下统一添加,以免后续难以维护。
之前文章也提到了 async/await,ES6、7都提出了不同的异步解决方案,可见前端对异步编程的重视程度,个人更看好Generator(迭代器),后面会写关于Generator的文章,敬请期待~