ES6 中 Promise 实现原理

这个很早以前写的,今天看群里有人问关于promise的问题,在这里重新发一下。偷懒的同学可以直接拉到最后有完整的代码。

顾名思义,promise中文意思就是承诺,也就是现在实现不了将来·····,但是将来这玩意谁说的准呢。就像你去泡妞,你可能许下各种诺言,但能不能实现,完全取决于你这人靠不靠谱。好在计算机不是人,不是人,不是人,····正因为不是人,所以它许下的承诺,它就一定会给你一个结果。 等待承诺实现的过程中很漫长,所以你可以做一些其它的事情,没必要老是堵在这一条道上,也就是异步。打个比方,你打电话给饭店老板叫了个外卖,老板告诉你,10分钟后送过去,也就是说老板给了你一个承诺,于是你等啊等,这中间又去上了个厕所,玩了会手机······,这就是异步,老板给的承诺并没有妨碍你干其它的事情。OK,扯淡结束。

promise这妞有啥好

为了实现异步,一般要设置一个回调函数

setTimeout(function(){

   console.log(1);

   setTimeout(function(){

       console.log(2);

       setTimeout(function(){

           console.log(3);

           setTimeout(function(){

               console.log(4);

               setTimeout(function(){

                   console.log(5);

               },500)

           },400)

       },300)

   },200)

},100);

······有没有一种想死的感觉! promise最大优势就是第一消灭了这种回调地狱,第二增加了错误捕获,像下面这种,

promise.then(function (response) {

   //do something;

}, function (reason) {

   //get error

}).then(function (response) {

   //do something;

}, function (reason) {

   //get error

}).then(function (response) {

   //do something;

}, function (reason) {

   //get error

});

如果不做错误处理则更清晰

promise.then(function (response) {

   //do something;

}).then(function (response) {

   //do something;

}).then(function (response) {

   //do something;

}).then(function (response) {

   //do something;

});

它使得异步的代码看起来像是在同步执行,大大增强了代码的可读性。美中不足的是你得写一堆的.then(function(){},function(){}),但是和回调地狱相比,忍了。在ES7中会有号称是异步的终极解决方案,async和await,那是后话。

这妞性格怎么样

前面说了,计算机不是人,所以它许下的承诺,它一定会给你一个结果,不管这个承诺的结果是接受还是拒绝。所以,第一,promise一定会返回一个结果。第二,这个结果是不可逆的,你只能接受,本质是因为promise的状态不可逆,一旦它变成了resolve或者reject,你休想再让你变成pending,否则,它要会说话,肯定回你的只有一个字,滚!第三、promise的结果什么时候返回,你说了不算,你去泡妞的时候,妞也不一定当场就答应你吧,或许想个三、五天也说不定,这个主动权不是掌握在你手中的。第四、ES6的promise执行过程中,你是无法获得执行的进度的,到底它现在是pending还是resolve,还是reject。就好像妞和她的闺蜜探讨要不要接受你,你是打听不到的。当然并不是完全不能,例如angularjs的$q实现一个notify方法,可以获取到执行进度的通知。最后说一点儿你的权力,你能决定的是在什么时候去取promise的结果,也就是调用then方法的时间,就好像你每天追着妞问,你想好没有······,妞这个时候会有三种回答,一是答应你,二是拒绝你,三是还得再想想,XXXX时候再告诉你····,也就说这TMD又是一个承诺·····。咳、咳,现在开始必须严肃点,毕竟技术是一件严肃的事情。

漂亮的妞,是个男人就会有想法

说白了,promise就是一个对象,一个只能通过then方法来取得它上面的值的对象。在es6中,你只要大喊一句,妞,给我个承诺,它就会给你一个promise,就像下面这样:

var promise = new Promise(function(resolve,reject){

   //do something;

})

然后你就可以调用它的then方法去取值,那么从这个角度看,这个构造函数一定是返回了一个带有then方法的对象。另外还有状态,状态的变化不可逆。再加上些其它的方法,如all、catch···,不过不要着急,我们一步一步来意淫出这个漂亮的妞····

通常情况,我们使用回调一个函数内执行另外一个函数:

function doSomething(callback){

   console.log("do something");

   callback();

}

doSomething(function(){

   console.log("a");

});

但是在使用promise时,我们是用then方法去取结果,而promise就是个对象,那么上面的代码看起来应该这样写:

function doSomething(){

   console.log("a");

   return {

       then: function(callback){

           var value = 1;

           callback(value);

       }

   }

}

doSomething().then(function(res){

   console.log(res);

});

在这里,我们调用dosomething函数时,返回了一个带有then方法的对象,然后在then方法回调中去执行,现在看来是不是有那么点样子了,时刻记得两件事,对象, then方法。

在ES6中Promise是一个构造函数,这简单,给这个dosomething换个名字,

function Promise(){

   this.then = function(callback){

       var value = 1;

       callback(value);

   }

}

在实例化promise的时候,要传一个函数进去,这也简单

function Promise(fn){

   this.then = function(callback){

       callback();

   }

}

实例化传入的函数fn中,下文中的fn都是指代这个匿名函数,你会传入2个参数,一个叫resolve,另一个叫reject,为了简单起见,我们不考虑reject,它的道理和resolve是一样的。那么就像这样:

var promise = new Promise(function(resolve){

   var value = 1;

   resolve(value);

})

即然传了一个fn函数进去,那么在实例化过程中,这个函数一定会在某个时刻执行。执行时,它又会接收到一个参数resolve,这个resolve一定是一个函数,这点从上面就可以很明显的看出来,resolve在实例化时执行了,而且接收到了一个参数,在这里是变量value。那么Promise函数内部很可能是这样:

function Promise(fn){

function resolve(value){

   }

   this.then = function (onResolved) {

   };

   fn(resolve);

}

为了看起来更直接,这里我们把调用then方法传的第一个函数就叫做onResolved,那么接下来我们应该考虑在实例化的时候,还有什么事情要做,在then方法的回调函数中我们希望得到promise的值,这个值是在fn函数调用后被resolve函数运算后得到的,最终要在onResolved函数中拿到,也就是说,我们必须在resolve中将这个值传递给onResolved,迂回一下:

function Promise(fn) {

var callback = null;

function resolve(value) {

       callback(value);

   }

this.then = function(onResolved) {

   callback = onResolved;

};

fn(resolve);

}

但是这里有一个问题,就是我们调用resolve方法时,还没有调用过then方法,因此callbak是null,浏览器报错:callback is not a function,这里hack下,让resolve方法的执行在then之后。

function Promise(fn) {

   var callback = null;

   function resolve(value) {

       setTimeout(function(){

           callback(value);

       },0)

   }

   this.then = function(onResolved) {

       callback = onResolved;

   };

   fn(resolve);

}

执行一下,

var promise = new Promise(function(res){

       var value = 2;

           res(2);

   });

promise.then(function(res){

   console.log(res);

})

OK,成功的输出。目前为止,promise的轮廓算是被我们意淫出来了。

promise是有状态的,而且状态不可逆,同样的为了简单起见,我先来搞定从pending变到resolved,那么rejected也一样。仔细想下,执行了resolve方法后可以得到一个resolved状态的值,那么必然在resolve方法中会去改变promise的状态,并且得到这个值,那么代码貌似应该这样写:

function Promise(fn) {

   var state = 'pending';

   function resolve(newValue) {

       state = 'resolved';

       callback(newValue);

   }

   this.then = function(onResolved) {

       callback = onResolved;

   };

   fn(resolve);

}

这里我们先把setTimeout这家伙给干掉了,因为我们加入了状态,也就意味我们是想通过状态的变化来知道能不能得到值,那么问题来了,我们不知道状态啥时候变,就像你不知道你要泡的妞啥时候答应你一样,你只能追问,万一妞没想好,她很可能再给你一个承诺,就是那个该死的XXX时候再告诉你,不过好歹她也算给了你一个等待的机会,而我们现在要做的就是创造这么个机会。

function Promise(fn) {

   var state = 'pending';

   var value;

   var deferred;

   function resolve(newValue) {

       value = newValue;

       state = 'resolved';

       if(deferred) {

       handle(deferred);

       }

   }

   function handle(onResolved) {

       if(state === 'pending') {

       deferred = onResolved;

       return;

       }

       onResolved(value);

   }

   this.then = function(onResolved) {

       handle(onResolved);

   };

   fn(resolve);

}

这里引入了另外一个函数handle,至此可以说promise的最关键的东西我们已经看到了,妞的身材逐渐显现。又扯远了·····仔细看下除了handle我们还引入两个变量value和deferred,先从最简单的来:value的作用很简单,在构造函数内它是一个全局变量,起到一个桥梁作用,就是为了在handle函数内能取到newValue的值,而newValue就是fn函数里的那个结果。handle我们估且可以认为它是妞的一个管家,它会去替我们询问妞有没有想好,也就是去判断当前这个承诺的状态,再决定怎么做。deferred我们估且可以这样理解,它就是管家的一个记事本,你隔三差五的去问,它老人家不得记下来,如果一不小心忘了,那就悲催了。这下不管是同步还是异步,我们随时可以在then方法中去取值,如果值没有被resolve,也就是说状态没发生变化,deferred将给我们记录下这件事,等到resolve的那个时间点把值传给then方法中那个回调函数,onResolved。在这里请默念一百遍handle,defer,再接着往下看,我保证他们会让你困惑。

回到最初,为什么要用promise,想想回调地狱,再想想promise是怎么解决的,那就是then方法链式调用。能够实现链式调用,也就是说then方法返回的值也一定是个promise,这样你才能.then,.then的一直写下去。废话不说,没代码说个毛:

function Promise(fn) {

   var state = 'pending';

   var value;

   var deferred = null;

   function resolve(newValue) {

       value = newValue;

       state = 'resolved';

       if(deferred) {

           handle(deferred);

       }

   }

   function handle(handler) {

       if(state === 'pending') {

           deferred = handler;

           return;

       }

       if(!handler.onResolved) {

           handler.resolve(value);

           return;

       }

       var ret = handler.onResolved(value);

       handler.resolve(ret);

   }

   this.then = function(onResolved) {

       return new Promise(function(resolve) {

           handle({

               onResolved: onResolved,

               resolve: resolve

           });

       });

   };

   fn(resolve);

}

这下换个姿势,我们先啃硬货。我们让then方法返回了一个promise,而且这个promise实例化时传入的函数里调用了handle函数,传入了一个对象,onResolved很显然就是then方法里第一个函数,没什么可说的。关键是这handle和resolve是哪个?思考1分钟。这里我们用setTimeout简单模拟一个异步,拿一个then看下,发生了什么:

var promise = new Promise(function(resolve){

       setTimeout(function(){

           resolve(1);

       },3000)

   });

promise.then(function(res){

   console.log(res);

})

首先我们去new一个promise,在实例化的过程中,调用了传进的那个函数,3秒后才能执行到resolve,紧接着调用了它的then方法,这个时候由于promise的状态没变,肯定取不到值,好在then方法会返回个promise,于是又执行了一次promise的实例化过程。这里无法回避的就是作用域的问题,这个关系到handle函数执行在哪个环境中,参数的到底从哪个地方获取到,另外就是强大的闭包。相关知识不解释。为了看的更清楚,我们加入一些标记,到chrome的控制台中调试下:let count = 0;

function Promise(fn) {

   let state = 'pending';

   let value;

   let deferred = null;

   let scope = ++count;

   function resolve(newValue) {

       value = newValue;

       state = 'resolved';

       console.log('resolve: I am in ' + scope);

       if(deferred) {

           handle(deferred);

       }

   }

   function handle(handler) {

       console.log('handle: I am in ' + scope);

           if(state === 'pending') {

           deferred = handler;

           return;

       }

       if(!handler.onResolved) {

           handler.resolve(value);

           return;

       }

       const ret = handler.onResolved(value);

       handler.resolve(ret);

   }

   this.then = function(onResolved) {

       console.log('then: I am in ' + scope);

       return new Promise(function(resolve) {

           console.log('then promise: I am in ' + scope);

           handle({

               onResolved: onResolved,

               resolve: resolve,

           });

       });

   };

   fn(resolve);

}

const promise = new Promise(function(resolve) {

   setTimeout(function() {

   resolve(1);

   }, 3000);

});

promise.then(function(res) {

   console.log(res);

}

看一看到底发生了什么

上面代码加入的scope是为了监视作用域的变化,以间接反应出我们调用handle时是在哪个作用域上查询到的,此外我们还需要监视state和deferred的变化。主要看then调用之后,废话不说上图:

在执行then方法的时候,scope=1,state,deferred不可用。由于模拟了异步,这个时候第一个promise的resolve方法并没有执行,这里模拟了3秒,实际情况下,比如ajax取数据时,我们并不知道这个准确的时间,就像开始时说的,这妞啥时候答应你,主动权不在你手中,由妞说了算。

接下来去实例化then方法创建的这个promise,scope = 2,state=”pending”,deferred=null。

在实例化完成之后,此时去执行fn函数,scope=1,state,deferred不可用。

第一,函数的作用域是在定义时就生成的,而不是在调用的时候。第二个promise定义的时候,是在第一个promise作用域上,这样即使它被return了出去,由于闭包的特性,仍读取的是第一个作用域上值,所以这里的handle必定是第一个promise的handle。而resolve则不同,它是作为行参传递了进来,所以这里的resolve是第二个promise的resolve。

进入handle时,scope = 1,state =” pending”,deferred保存了参数。

3秒时间到,第一个promise里的resolve被执行了,也就是说拿到了结果,这时候,scope=1,state = “resolved”,deferred保存着刚才传进来的那个对象,再次进入handle函数。

scope=1,state = “resolved”,deferred求值为true,因此肯定会继续执行。下面添加的这段代码在这里也就很清楚了,假如then方法中没有传进来的onResolved函数,这里的value将直接交给下一个then方法中的onResolved函数使用,避免一些无聊的人像这样去调用:promise.then().then().then(function(res){console.log(res);})正常人都会让value在onResolved函数中接收到,然后ret就是onResolved函数的返回值,这里没有return回的值,所以ret肯定是undefined。

scope=2,state = “resolved”,deferred=null。这里的resolve是第个promise的resolve,所以定义的时候就是在作用域2上,如果后面再调用then方法,生成新的promise,这时就会将undefined作为第二个promise的值传递下去。

这里再次强调一下,handle方法和deferred是核心所在,其背后的精髓无非还是作用域和闭包的巧妙设计。变量的读取必定先从自身所处作用域开始,如果自身作用域上读不到,才会一级一级向上访问。

完整代码:

function Promise(fn) {

   var state = 'pending';

   var value;

   let deferred;

   this.then = function (onResolved, onRejected) {

       return new Promise(function (resolve, reject) {

       handle({

           onResolved: onResolved,

           onRejected: onRejected,

           resolve: resolve,

           reject: reject,

           });

       });

   };

   function resolve(newValue) {

       if (newValue && typeof newValue.then === 'function') {

           newValue.then(resolve);

           return;

       }

       state = 'resolved';

       value = newValue;

       if (deferred) {

           handle(deferred);

       }

   }

   function reject(reason) {

       state = 'rejected';

       value = reason;

       if (deferred) {

           handle(deferred);

       }

   }

   function handle(handler) {

       if (state === 'pending') {

           deferred = handler;

           return;

       }

       var handlerCallback;

       if (state === 'resolved') {

           handlerCallback = handler.onResolved;

       }else {

           handlerCallback = handler.onRejected;

       }

       if (!handlerCallback) {

           if (state === 'resolved') {

               handler.resolve(value);

           } else {

               handler.reject(value);

           }

           return;

       }

       var ret;

       try {

           ret = handlerCallback(value);

       }catch (e) {

           handler.reject(e);return;

       }

       handler.resolve(ret);

   }

   fn(resolve);

}

情况基本和resolve是一样的,resolve函数中加的if判断只为了对付返回值是promise的情况下仍然可以通过后续的then方法取到值,handle中的try/catch块的加入使得可以捕获到promise及then方法回调中的错误,至于then方法的改变,看不懂的话自宫吧,你是女人当我没说。

其它

当然这个promise只是一个基本的实现,依然很脆弱,但基本上可以说有了一轮廓,剩下的部位各位看官自己添加,比如promise的all ,race,catch等。某种意义上说,它们也只是then方法的语法糖。 http://www.mattgreer.org/articles/promises-in-wicked-detail/,本文代码出处,有兴趣的同学可以看下。

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “小程序”,获取更多资料,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容