自己动手实现ES6 Promise

不管是标准还是实现,现在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.allPromise.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对象是有三种状态的, pendingfulfilledrejected。为什么要有状态?为了分情况处理。

先举一个例子,假如我定义一个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重新赋予了我们“正常”的函数,这是它更重要的意义。

原文:http://madmuggle.me/articles/ES6_Promise.html

    原文作者:madmuggle
    原文地址: https://www.jianshu.com/p/48e193c4662e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞