不管是标准还是实现,现在Javascript的重心都放在了async-await
上,Promise怎么看都像过时的东西。而且支持Promise的库有一大堆,就算不需要这些库,今天的浏览器和Node.js也已经原生支持Promise了。在这种前提下,为什么还要自己去实现一个Promise呢?
ES7的async-await建立在Promise上
客观来讲,由于await本身的特点,将来JS把底层API良好封装之后,即使用户完全不知道Promise,在使用上也不会有啥问题~
然而ES7的Async-Await,和Promise并不是毫不相关的竞争对手。实际上await
后面必须跟着一个Promise对象。所以深入理解Promise不会是毫无用处的事情。
Promise不需要编译器/解释器的支持
将来可能成为主流的async-await
,以及曾经火过一把的generator + co
,这些都是需要编译器或者解释器级别的支持才能使用。
而Promise,是完全可以利用语言已有特性,作为一个库来实现!即使在非常原始的JS运行环境,你也可以自己实现一个Promise,而不需要等待其他人的帮助。
Promise是语言无关的
Promise还是独立于语言的,如果你要给另外一种编程语言实现Promise,只要照葫芦画瓢就行了。
也就是说,掌握Promise的实现原理,是一种回报率非常高的通用型技能。而且只需要很少的投入,一百多行代码而已。(核心的其实只有几十行)
所以,让我们开始吧!
如何实现
对于Promise这种代码量不大,但是行为复杂的程序,最好的学习方法是直接看代码。只看别人的解释可能会弄得似懂非懂。
先放一个我自己的实现以供参考,建议还在网上搜一下其他人的实现。很多论坛里面都有人写简单的部分Promise实现
,大都有借鉴价值。通过看不同人的写法,可以更容易理解其核心部分。我在实现自己的Promise时,就看过了很多片段代码,然后才慢慢知道该怎么做。
最核心的方法
最核心的一个方法是Promise.prototype.then
,实现了它,也就实现了Promise的一大半。如果再把Promise.all
和Promise.race
实现了,你基本上就实现了完整的Promise,因为剩下的,都是简单的封装而已。
then
每次调用then
,你都在创建一个新的Promise对象。then
就像一个锁链一样,将前后的两个Promise
对象连接起来。
为了突出这一点,我在自己的实现里面,特意把逻辑代码外移,下面是代码片段
Promise.prototype.then = function(resolveFn, rejectFn) {
var pP = this
return new Promise((res, rej) => thenHandler(res, rej, resFn, rejFn, pP))
}
all
调用Promise.all
,也会返回一个新的Promise对象,all
后面的then
,是挂在这个新的Promise对象上的。
Promise.all = function(promises) {
checkArray(promises)
return new Promise((res, rej) => handleAll(promises, res, rej))
}
pending, fulfilled, rejected
要理解Promise,另一个关键在于理解Promise的「状态」。一个Promise对象是有三种状态的, pending
,fulfilled
和rejected
。为什么要有状态?为了分情况处理。
先举一个例子,假如我定义一个Proimse,但是不给它绑定then
var x = myReadFile("/tmp/text.txt")
这条语句在运行的时候,那个文件的内容其实已经在某个时间点读出来了。一直缓存在某个地方。吃完饭我们再来运行:
x.then(console.log)
//> Promise { <pending> }
// blahblah..., the content of /tmp/text.txt
数据全都出来了!因为then
会对Promise对象的「状态」进行判断。如果是pending
状态,就把将要运行的函数存到Promise对象的一个数组里面,如果是fulfilled
状态,也就是那个Promise的resolve
已经被运行了,那么就直接调用then
传来的函数。
这就是状态存在的意义。
然后就可以直接看代码了,需要的就是适当的耐心,乐观的态度~
如果读者对Promise不了解,想知道它运行的一些特点,那么可以继续往下看。
Promise的运行机理
接下来,我们通过两个例子来探索Promise的运行过程,为了减少重复代码,我们先定义一个函数,这个函数会返回一个Promise对象,这就是一切的开端。
function myReadFile(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (e, d) => e ? reject(e) : resolve(d.toString()))
})
}
使用Promise,代码经常看上去会是这个样子的
myReadFile("theFirst.json")
.then(JSON.parse)
.then(fn1)
.then(fn2)
.then(fn3)
.catch(console.error.bind(console))
上面这个例子中,真正能够异步的,只有第一步而已。一旦那个resolve
被调用,后面的一连串都会顺着执行。就像多米诺骨牌一样。
那么如果中间有另一个地方需要异步怎么办呢,比如我需要读取另外一个文件? 你只需要在某个传给then
的函数里面,返回一个新的Promise对象就行。
myReadFile("theFirst.json")
.then(JSON.parse)
.then(fn1)
.then(d => myReadFile("theSecond.json"))
.then(JSON.parse)
.then(fn3)
.catch(console.error.bind(console))
上面这个例子中,fn3
处理的是theSecond.json
文件的内容。
这一点可能理解上有点别扭,大概需要实现了Promise之后,才能清楚其中的猫腻。
关于Promise,我能想到的需要注意的,暂时就这么多了。以后如果有新的点子,会继续放上来。
Promise的缺点
我对Promise非常喜爱,但是它的确有缺点,比如一些中间变量无法共用,我们拿同步
例子来做个对比
var a = readFileSync("blahblah.txt")
var b = fn1(a)
var c = fn2(b)
var d = fn3(c)
console.log(a, b, c, d)
而使用Promise的异步则是这个情况
myReadFile("blahblah.txt")
.then(fn1)
.then(fn2)
.then(fn3)
.then(d => console.log(d))
其中fn1
的返回值只有fn2
能获取,fn2
的返回值只有fn3
能获取…… 如果需要像同步版本那样,获取所有中间值,就必须把它们存为全局或者上层闭包变量。
但是这个小小的缺点不太要紧。瑕不掩瑜,Promise彻底解决了callback hell
,让我对Javascript另眼相看。
后记
随着对Promise使用时间的增长,我意识到了Promise的一些其他优点。它不仅仅是解决了回调的“代码金字塔”问题。应该说,回调带来的真正问题,并不是代码不停往右边延展,而是你不能以正常的「函数」概念来思考问题。
什么是函数?我记得以前数学老师向我们解释:函数就是一个工厂,你给一个毛胚进去,它变出一个产品。
当时我还没有编程的概念,当我学会编程之后,更加赞同那个朴实的比喻了。函数这东西,就是做转换,它不仅要有输入,还要有输出。
而回调,是“没有”输出的。
它当然有输出,只不过很别扭,因为它不是作为返回值呈现,而是通过你传给它的另一个输入(回调函数)来处理。是不是隐约找到当年C语言那堆库函数在你心中留下的伤疤。
Promise重新赋予了我们“正常”的函数,这是它更重要的意义。