本文同步自个人我的博客: http://mly-zju.github.io/javascript
众所周知javascript语言的一大特点就是异步,这既是它的优势,同时在某些状况下也带来了一些的问题。最大的问题之一,就是异步操做过多的时候,代码内会充斥着众多回调函数,乃至造成回调金字塔。为了解决回调函数带来的问题,Promise做为一种更优雅的异步解决方案被提出,最初只是一种实现接口规范,而到了es6,则是在语言层面就原生支持了Promise对象。 java
最初接触Promise的时候,我以为它是比较抽象而且使人困惑的,相信不少人也有一样的感受。可是在后来的熟悉过程当中,我慢慢体会到了它的优雅,并开始思考Promise对象实现的原理,最终用es5语法实现了一个具有基本功能的本身的Promise对象。在这篇文章中,会把本身实现的过程和思路按部就班的记录一下,相信你们看完以后,也可以完全理解Promise对象运行的原理,并在之后的开发中,能更熟练的使用它。git
github源码地址: https://github.com/mly-zju/Js-practicees6
首先来看一个Promise的使用实例:github
var fn=function(resolve, reject){ console.log('begin to execute!'); var number=Math.random(); if(number<=0.5){ resolve('less than 0.5'); }else{ reject('greater than 0.5'); } } var p=new Promise(fn); p.then(function(data){ console.log('resolve: ', data); }, function(data){ console.log('reject: ', data); })
这个例子当中,在fn当中产生一个0~1的随机数,若是小于等于0.5, 则调用resolve函数,大于0.5,则调用reject函数。函数定义好以后,用Promise包裹这个函数,返回一个Promise对象,而后调用对象的then方法,分别定义resolve和reject函数。这里resolve和reject比较简单,就是把传来的参数加一个前缀而后打印输出。less
这里咱们须要注意,当运行 p=new Promise(fn)这条语句的时候,fn函数就已经在执行了,然而,p.then这个方法是在后面才定义了resolve和reject,那么为什么fn函数可以知道resolve和reject函数是什么呢?dom
换句话说,resolve和reject函数是如何回到过去,出如今先执行的fn函数当中的呢?这是Promise当中最重要的一个概念之一。异步
其实想要实现这个“黑科技”,方法也很是简单,主要运用的就是setTimeout这个方法,来延迟fn当中resolve和reject的执行。利用这个思路,咱们能够初步写出一个本身的初级版Promise,这里咱们命名为MyPromise:函数
function MyPromise(fn) { this.value; this.resolveFunc = function() {}; this.rejectFunc = function() {}; fn(this.resolve.bind(this), this.reject.bind(this)); } MyPromise.prototype.resolve = function(val) { var self = this; self.value=val; setTimeout(function() { self.resolveFunc(self.value); }, 0); } MyPromise.prototype.reject = function(val) { var self=this; self.value=val; setTimeout(function() { self.rejectFunc(self.value); }, 0); } MyPromise.prototype.then = function(resolveFunc, rejectFunc) { this.resolveFunc = resolveFunc; this.rejectFunc = rejectFunc; } var fn=function(resolve, reject){ console.log('begin to execute!'); var number=Math.random(); if(number<=0.5){ resolve('less than 0.5'); }else{ reject('greater than 0.5'); } } var p = new MyPromise(fn); p.then(function(data) { console.log('resolve: ', data); }, function(data) { console.log('reject: ', data); });
能够看出, MyPromise接收fn函数,并将本身的this.resolve和this.reject方法做为fn的resolve和reject参数传给fn并执行。而咱们观察MyPromise的resolve方法,即可以发现,其主要操做,就是使用setTimeout,延迟0秒执行resolveFunc。this
而再来观察then方法,能够看到,这里比较简单,就是接受两个函数,并分别赋给自身的this.resolveFunc和this.rejectFunc。
这里逻辑就很清楚了,虽然fn函数首先执行,可是因为在调用resolve和reject的时候,使用了setTimeout。虽然是延迟0秒执行,可是咱们知道js是单线程+消息队列,必须等主线程代码执行完毕才能开始执行消息队列当中的代码。所以,会首先执行then这个方法,给resolveFunc和rejectFunc赋值。then执行完毕后,再执行setTimeout里面的方法,这个时候,resolveFunc和rejectFunc已经被赋值了,因此就能够顺利执行。这就是“回到过去”的奥秘所在。
上一节,初步实现了看起来彷佛可以运行的MyPromise,可是问题不少。咱们看一下下面代码:
var fn=function(resolve, reject){ resolve('hello'); reject('hello again'); } var p1=new Promise(fn); p1.then(function(data){ console.log('resolve: ',data) }, function(data){ console.log('reject: ',data) }); //'resolve: hello' var p2=new MyPromise(fn); p2.then(function(data){ console.log('resolve: ',data) }, function(data){ console.log('reject: ',data) }); //'resolve: hello ' //'reject: hello again'
p1是原生Promise,p2是咱们本身写的,能够看出,当调用resolve以后再调用reject,p1只会执行resolve,咱们的则是两个都执行。事实上在Promise规范当中,规定Promise只能从初始pending状态变到resolved或者rejected状态,是单向变化的,也就是说执行了resolve就不会再执行reject,反之亦然。
为此,咱们须要在MyPromise中加入状态,并在必要的地方进行判断,防止重复执行:
function MyPromise(fn) { this.value; this.status = 'pending'; this.resolveFunc = function() {}; this.rejectFunc = function() {}; fn(this.resolve.bind(this), this.reject.bind(this)); } MyPromise.prototype.resolve = function(val) { var self = this; if (this.status == 'pending') { this.status = 'resolved'; this.value=val; setTimeout(function() { self.resolveFunc(self.value); }, 0); } } MyPromise.prototype.reject = function(val) { var self = this; if (this.status == 'pending') { this.status = 'rejected'; this.value=val; setTimeout(function() { self.rejectFunc(self.value); }, 0); } } MyPromise.prototype.then = function(resolveFunc, rejectFunc) { this.resolveFunc = resolveFunc; this.rejectFunc = rejectFunc; }
这样,再次运行上面的实例,就不会出现resolve和reject都执行的状况了。
在Promise的使用中,咱们必定注意到,是能够链式调用的:
var fn=function(resolve, reject){ resolve('hello'); } var p1=new Promise(fn); p1.then(function(data){ console.log(data); return 'hello again'; }).then(function(data){ console.log(data); }); //'hello' //'hello again'
很显然,要实现链式调用,then方法的返回值也必须是一个Promise对象,这样才能再次在后面调用then。所以咱们修改MyPromise的then方法:
MyPromise.prototype.then = function(resolveFunc, rejectFunc) { var self = this; return new MyPromise(function(resolve_next, reject_next) { function resolveFuncWrap() { var result = resolveFunc(self.value); resolve_next(result); } function rejectFuncWrap() { var result = rejectFunc(self.value); resolve_next(result); } self.resolveFunc = resolveFuncWrap; self.rejectFunc = rejectFuncWrap; }) }
这里能够看出,then返回了一个MyPromise对象。在这个MyPromise当中,包裹了一个函数,这个函数会当即执行,主要作的事情,就是对resolveFunc和rejectFunc进行封装,而后再赋值给前一个MyPromise的resolveFunc和rejectFunc。这里难点是看懂封装的目的。
这里以上面一个例子来讲明。在上面的链式调用例子中,出现了两个Promise,第一个是咱们经过new Promise显式定义的,咱们叫它Promise 1,而第二个Promise,是Promise 1的then方法返回的一个新的,咱们叫它Promise 2 。在Promise 1的resolve方法执行以后,resolve的返回值,会传递给Promise 2的resolve做为参数,这也是为何上面第二个then中打印出了第一个then返回的字符串。
而咱们封装的目的,就是为了让Promise 1的resolve或者reject在执行后,将其返回值传递给Promise 2的resolve。在咱们本身的实现中,Promise 2的resolve咱们命名为resolve_next,在Promise 1的resolveFunc执行以后,咱们拿到返回值result,而后调用resolve_next(result),传递参数给Promise 2的resolve。这里值得注意的是,不管Promise 1执行的是resolveFunc仍是rejectFunc,其以后调用的,都是Promise 2的resolve,至于Promise 2的reject用来干吗,在下面的章节里面咱们会详细描述。
至此,咱们的MyPromise看起来就可使用链式调用了。
然而咱们再回去观察Promise规范,会发现链式调用的状况也分两种。一种状况下,前一个Promise的resolve或者reject的返回值是普通的对象,这种状况下咱们目前的MyPromise能够正确处理。但还有一种状况,就是前一个Promise的resolve或者reject执行后,返回的值自己又是一个Promise对象,举个例子:
var fn=function(resolve, reject){ resolve('hello'); } var p1=new Promise(fn); p1.then(function(data){ console.log(data); return 'hello again'; }).then(function(data){ console.log(data); return new Promise(function(resolve){ var innerData='hello third time!'; resolve(innerData); }) }).then(function(data){ console.log(data); }); //'hello' //'hello again' //'hello third time!'
在这个例子当中出现了两次链式调用,第一个then返回的是一个'hello again'字符串,在第二个then的resolve中会打印处理。而后咱们注意第二个then当中,返回的是一个Promise对象,调用了resolve。那么问题来了,这个resolve哪里来呢?答案就是在第三个then当中定义!这个例子中第三个then定义的resolve也比较简单,就是直接打印传给resolve的参数。
所以,这里咱们的MyPromise也须要修改,针对前一个resolve或者reject的返回值作判断,看是否是Promise对象,若是是,就作不一样的处理,修改的代码以下:
MyPromise.prototype.then = function(resolveFunc, rejectFunc) { var self = this; return new MyPromise(function(resolve_next, reject_next) { function resolveFuncWrap() { var result = resolveFunc(self.value); if (result && typeof result.then === 'function') { //若是result是MyPromise对象,则经过then将resolve_next和reject_next传给它 result.then(resolve_next, reject_next); } else { //若是result是其余对象,则做为参数传给resolve_next resolve_next(result); } } function rejectFuncWrap() { var result = rejectFunc(self.value); if (result && typeof result.then === 'function') { //若是result是MyPromise对象,则经过then将resolve_next和reject_next传给它 result.then(resolve_next, reject_next); } else { //若是result是其余对象,则做为参数传给resolve_next resolve_next(result); } } self.resolveFunc = resolveFuncWrap; self.rejectFunc = rejectFuncWrap; }) }
能够看到在代码中,对于resolveFunc或者rejectFunc的返回值,咱们会判断是否含有.then方法,若是含有,就认为是一个MyPromise对象,从而调用该MyPromise的then方法,将resolve_next和reject_next传给它。不然,正常对象,result就做为参数传给resolve_next。
这样修改以后,咱们的MyPromise就能够在链式调用中正确的处理普通对象和MyPromise对象了。
如此,在这篇文章中,咱们就首先实现了Promise的经常使用基本功能,主要是then的调用,状态的控制,以及链式调用。而在后面的文章中,还会进一步讲解如何实现Promise的错误捕获处理等等(好比Promise当中的.catch方法原理),从而让咱们的MyPromise真正健壮和可用!