本作品采用知识共享署名 4.0 国际许可协议进行许可。转载保留声明头部与原文链接https://luzeshu.com/blog/bluebirdsource 
本博客同步在http://www.cnblogs.com/papertree/p/5328134.html


  今天把 bluebird(2.9.0版) 的源码看了,写成博客记录一下。

1. 带上几个问题看源码

1. promise链是如何实现的?

2. promise对象如何变成fulfill状态,并触发promise链条的后续函数?new Promise和Promise.resolve() 有何不同?

*3. 为什么每执行.then就创建一个新的Promise对象,而不能使用第一个promise依次.then?

4. 如何对throw Error 和 reject进行加工

  分两条主线来讲解:

第2节:回调函数的设置、promise链条的保存

第3节:promise对象的解决(设置为rejected、fulfilled)、链条的迁移


2. promise链如何实现 —— 注册阶段(.then)

  我们都知道设置一个promise链是通过promise对象的.then方法注册fulfill、reject 状态被激活时的回调函数。来看一下.then的代码:

图2-1

2.1 promise保存链条的结构

  上图可以看到.then内部调用了._then,然后把我们传给.then()函数的didFulfill、didReject等回调函数通过_addCallbacks保存下来。这里注意到,不是通过 “ this._addCallbacks() ”,而是通过 “ target._addCallbacks() ”,而且上一行还判断了 “ target !== this ”的条件。那么target是什么呢?待会2.5节讲。

  看到 _addCallbacks的实现,promise对象以每5个参数为一组保存。当对一个promise对象调用一次.then(didFulfill, didReject)的时候,这组相关的参数保存在:

this._fulfillmentHandler0;  // promise对象被置为fulfilled 时的回调函数this._rejectionHandler0;  // promise对象被置为rejected 时的回调函数this._progressHandler0;this._promise0;this._receiver0;  // 当 fulfill被调用时  ,传给函数的 this对象

代码2-1

  当在一个promise对象上超过一次调用.then(didFulfill, didReject) 时,大于1的部分以这种形式保存在promise对象上:

var base; // base表示每组参数的起点,每5个参数为一组保存
this[base + 0];
this[base + 1];
this[base + 2];
this[base + 3];
this[base + 4];

代码2-2

2.2 链条的拓扑结构 —— 为何每个.then 都new一个新的Promise对象?

  很多说明文档会给出这样的示例代码:

// 来自 http://liubin.org/promises-book/#ch2-promise.then
// promise可以写成方法链的形式

aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
    console.log(error);
});

代码2-3

  这样的实现的任务块是这样一种拓扑结构:

图2-2

  而对于另一种拓扑结构的任务,有all 和 race方法:

  

图2-3

  如果没有深究,咋一看可能以为上面的“代码2-3”中,依次.then都是在同一个aPromise对象上,而.then所注册的多个回调函数都保存在aPromise上。

  事实上,看到上面图2-1中,Promise.prototype._then的代码里面,每次执行_then都会新建一个Promise对象,比如代码2-3实际上等效于这样:

var bPromise = aPromise.then(function taskA(value){
// task A
});
var cPromise = bPromise.then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
    console.log(error);
});

代码2-4

  aPromise、bPromise、cPromise分别是不同的对象。

  那么为什么这么实现呢?想一下就会知道这样多种拓扑结构:

图2-4

  当在同一个promise对象上多次执行.then时,跟代码2-3依次.then的情况并不一样,如下的示例代码:

var bPromise = aPromise.then(function taskA(value){  // task A
    return new Promise(function (resolve) {
        setTimeout(function () {
            return resolve();
        }, 5000);
    });
});
var cPromise = aPromise.then(function taskB(vaue){  // task B
  console.log('task B');
}); 

代码2-5

  这里用aPromise.then两次,注册两个onFulfill函数(function taskA 和 function taskB)。当task A 里返回新建的promise对象处于pending状态时,task B的任务会先执行。

  

  那么这样的promise链条是相当灵活的,可以实现任何网状的依赖关系。那么通过这个发现,我想到利用它来做一件有趣的事情,可以求有向图最短路径的值,看2.3节。

2.3 利用promise的拓扑特性做有趣的事 —— 有向图的最短路径另类求值

图2-5

  如上这个有向图,要求0到3的最短路径,那么你可能第一想到的是Dijkstra算法、Floyd算法等等。

  那么利用promise在2.2节中讲的特性,刚好可以用来求最短路径的值。但这里只是求值(玩玩),不能代替“最短路径算法”。上代码:

 var Promise = require('bluebird');

 var _base = 10;  // 等待时间基数

 var dot0 = new Promise(function (resolve) {
     return resolve('0');
 });

 var dot0_2 = dot0.then(function () {
     return new Promise(function (resolve) {
         setTimeout(function() {
             return resolve('0');
         }, 5 * _base);
     });
 });

 var dot0_3 = dot0.then(function () {
     return new Promise(function(resolve) {
         setTimeout(function () {
             return resolve('0');
         }, 30 * _base);
     });
 });

 var dot2 = Promise.race([dot0_2]);

 var dot2_1 = dot2.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 2');
         }, 15 * _base);
     });
 });

 var dot2_5 = dot2.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 2');
         }, 7 * _base);
     });
 });

 var dot5 = Promise.race([dot2_5]);

 var dot5_3 = dot5.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 5');
         }, 10 * _base);
     });
 });

 var dot5_4 = dot5.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 5');
         }, 18 * _base);
     });
 });

 var dot1 = Promise.race([dot2_1]);

 var dot1_4 = dot1.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 1');
         }, 8 * _base);
     });
 });

 var dot4 = Promise.race([dot1_4, dot5_4]);

 var dot4_3 = dot4.then(function (which) {
     return new Promise(function (resolve) {
         setTimeout(function () {
             return resolve(which + ' 4');
         }, 4 * _base);
     });
 });

 var dot3 = Promise.race([dot0_3, dot4_3, dot5_3])
     .then(function (str) {
         console.log('result: ', str + ' 3');
     });

// 输出结果:// 0 2 5 3

代码2-6

  如果我们把2->1边的权值改成4,即把第31行代码的15改成4,那么输出结果会是 : 0 2 1 4 3

  换种写法(结果一样):

 var Promise = require('bluebird');

 var _base = 10;
 // key表示顶点,值表示出边
 var digram = {
     '0': { '2': 5, '3': 30 },
     '2': { '1': 15, '5': 7 },
     '5': { '3': 10, '4': 18 },
     '1': { '0': 2, '4': 8 },
     '4': { '3': 4 },
     '3': {}
 };
 var order = ['0', '2', '5', '1', '4', '3'];
 var startDot = '0';
 var endDot = '3';

 var promiseMap = {};
 function _buildMap() {
     for(var dot in digram)
         promiseMap[dot] = {_promise: undefined, _in: [], _out: []};
     for(var i = 0 ; i < order.length; ++i) {    // 这里不能用 for(var dot in digram),因为js对map的key会排序,这样取出来的dot顺序是0、1、2、3、4、5
         var dot = order[i];
         if (dot == startDot) {
             promiseMap[dot]._promise = Promise.resolve();
         } else if (dot == endDot) {
             var localdot = dot;
             promiseMap[dot]._promise = Promise.race(promiseMap[dot]._in)
                 .then(function (str) {
                     console.log('result: ', str + ' ' + localdot);
                 });
             continue;
         } else {
         debugger;
             promiseMap[dot]._promise = Promise.race(promiseMap[dot]._in);
         }
         for(var edge in digram[dot]) {
             var edgePromise =
                 promiseMap[dot]._promise.then(function (which) {
                     var self = this;
                     return new Promise(function (resolve) {
                         setTimeout(function () {
                             return resolve( (which ? which + ' ' : '') + self.dot);
                         }, digram[self.dot][self.edge] * _base);    // 这里不能直接访问外层dot、edge,因为异步函数被调用的时候值已经被改变,也无法通过for循环里面保存tmpdot、tmpedge的办法,因为js没有块级作用域,es6新标准有块级作用域
                     });
                 }.bind({dot: dot, edge: edge}));
             promiseMap[dot]._out.push(edgePromise);
             promiseMap[edge]._in.push(edgePromise);
         }
     }
 }
 _buildMap();

// 输出结果:// 0 2 5 3

代码2-7

2.4 .then链条的结构

  那么通过2.1、2.2节的理解,我们知道了,一个.then链条里面的结构并不是这样:

图2-6

  这是在同一个promise对象上多次.then的情况(代码2-5)。

  而依次.then的链条(代码2-3 / 代码2-4)是这样的:

图2-7

  就是说如果这样的代码,不使用同一个promise对象,去.then两次,那么2.1中_addCallbacks的结构只会用到【this._promise0、】这一组,而不会有【this[base + index]】这些数据。

2.5 Promise.prototype._target()

  2.1节留了一个疑问,在调用promise.then注册一个回调函数的时候,不是通过“ this._addCallbacks() ” 而是通过 “target._addCallbacks() ”,那么这个target是什么?

  通过上几节,了解了内部链条保存的细节,现在来看一下target。

  看个示例代码:

图2-8

  那么通过app2.js,可以看到一般情况下,aPromise._target() 取到的target是this对象。通过target(aPromise)调用_addCallbacks时,bPromise是存在aPromise._promise0里面的。

  通过app3.js,可以发现,当对aPromise使用一个pending状态的cPromise对象进行resolve时,aPromise._target()取到的target会变成cPromise,后续通过aPromise.then所创建的bPromise对象也都是通过target(cPromise)进行_addCallbacks的,这个时候aPromise._promise0就是undefined,而cPromise._promise0就是bPromise。

  那么这里target的变动与promise链条的迁移如何实现呢?这里涉及到解决(settle)一个promise对象的细节,第3节会再讲到。


3. promise对象的resolve细节 —— 解决阶段(.resolve)

3.1 resolve一个promise对象的几种情况

  看下示例代码:

var Promise = require('bluebird');

var aPromise = new Promise(function (resolve) {
    return resolve();  // resolve的调用可能在任何异步回调函数里面
});

var bPromise = aPromise.then(function () {
    var dPromise = Promise.resolve();
    return dPromise;
});

var cPromise = bPromise.then(function () {
    console.log('cPromise was resolved');
});

代码3-1

1. 构造函数的回调函数里面,通过resolve()由我们手动触发解决,例如上面的 aPromise。resolve可能在任何异步回调函数里面被调用。

2. 通过Promise.resolve()创建一个已经被解决的promise对象

3. then函数注册的回调函数,会在上游promise对象被解决掉之后,由promise的机制触发后续promise对象被解决。比如aPromise被resolve之后,bPromise、cPromise 由Promise的机制进行解决。

  这几种情况的细节在3.3节讲。

3.2 Promise的队列管理对象 —— async

  async是Promise用来管理promise链中所有promise对象的settle 的一个单例对象,在async.js文件:

图3-1

  async提供两个接口:

    1. settlePromises:接收一个promise对象,针对3.1节中的情况1,调用async.settlePromises去把要settle的promise对象入队

    2. invoke:接收一个回调函数的相关参数,针对3.1节中的情况2,把被settle的上游promise中保存的回调函数(2.1节中的参数组)通过async.invoke,把要执行的回调函数

3.3 resolve一个promise链的细节

  针对3.1节讲的几种情况,进行详细说明。

3.3.1 构造函数里的resolver

  来看代码:

图3-2

  看右上角的示例代码,右下角是输出结果,为什么“step 2”不是在“step 1”之前呢?可以知道构造一个Promise对象时,传进去的函数是被同步执行的(如果是异步执行的,那么“step 1”必定在“step 2”之后),这也意味着,如果在该回调函数里面同步调用resolve(),那么该Promise对象被创建之后就已经是fulfilled状态了。【看step 2 的输出】。

  可以从左边的源码看到,传给构造函数的回调函数是被同步执行的。

  可以看出构造函数“step 1”被调用的时机。

3.3.2 .then注册的回调函数被触发的机制 —— aPromise.then时,已是fulfilled状态

  那再来看上图3-2的示例代码中,通过aPromise.then()创建的bPromise对象。

  我们知道aPromise 变成fulfilled之后,通过aPromise.then注册的bPromise也是会被settle的。而在aPromise.then的时候,aPromise本身已经是fulfilled状态的。那么通过“step 3”的输出、已经“step 3”和“step 4”的顺序,可以知道通过.then()创建的promise对象的onFulfilled函数是被异步执行的(不管.then的时候aPromise是否fulfilled),而且通过“step 5”的输出,我们可以猜到这个异步大致也是通过process.nextTick() 处理的。

  我们来看看实现:

3.3.3 .then注册的回调函数被触发的机制 —— aPromise.then时,处于pending状态

  3.3.2中讲了aPromise为已经fulfilled时,.then产生的后续promise对象在 async.invoke(target._settlePromiseAtPostResolution, target, callbackIndex)中通过process.nextTick进行settle。

  那么如果“图3-2”的示例代码中,构造函数的resolver是这样的呢:

代码3-2

  那么aPromise.then产生bPromise时,aPromise还是pending状态,这时后续的bPromise对象的settle要等到aPromise被手动resolve()时再触发。

  来看实现:

3.3.4 .then注册的回调函数被触发的机制 —— bPromise.then时,bPromise本身就是.then产生的一个promise对象

3.3.5 .then链条的解决

  那么结合3.3.1 - 3.3.4,我们看这样一个promise链的解决时机是怎样的,示例代码:

var aPromise = new Promise(function (resolve) {
    return resolve();
})
.then(function () {        // 假设这里创建的是bPromise
   // task B
})
.then(function () {        // 假设这里创建的是cPromise
    // task C
});

  解决顺序:

1. aPromise创建之时,同步执行了构造函数的回调函数,同步执行了resolve。这个是3.3.1节的情况。

2. bPromise在创建的时候,aPromise已经为fulfilled状态,这时通过async.invoke(target._settlePromiseAtPostResolution, target, callbackIndex),把bPromise的settle任务放到process.nextTick。这个是3.3.2节的情况。

3. cPromise在创建的时候,注意这里cPromise不是通过aPromise.then产生的,而是bPromise.then产生的,那么这个时候bPromise还是pending状态的,所以cPromise的settle任务是3.3.4节里面的情况。

3.3.4 Promise.resolve()创建一个以解决的对象

  这种情况下类似于3.3.3中的,new Promise之后,在resolver里面同步resolve。

3.4 promise链的迁移

  回过来看2.1和2.5中提到的target的问题。看下Promise.prototype._target()的代码:

  

图3-

  promise对象内部的状态维护是通过一个 this._bitField属性,进行位运算去设置、判断状态的。看下相关代码:

图2-2

  可以看到除了fulfilled、rejected等状态,还有isMigrated 和Following 状态,那么这个是什么呢?因为这两个状态主要效劳于内部实现,所以一般的使用文档里面可能只会提及fulfill、reject、pending这几个状态。

  我们暂且来看看 this._target() 实现了什么:

图2-3

  可以看到,如果promise对象是Following状态的话,就会一直取自身的followee,直到followee的源头。而如果不是Following状态,那么在上面._then函数里,可以忽略取target这个步骤,因为取到的target是this。

  那么找一下promise什么时候被setFollowing,会发现就在3.3.1节中通过构造函数去resolveFromResolver的时候设置的,而且在特定的情况下发生:

    当resolve的参数是一个处于pending状态的promise对象时,就会把该promise对象上的后续promise迁移过去。这时看回去2.5节中示例代码,明白为什么了把?

promise/bluebird源码的更多相关文章

  1. Promise的源码实现(完美符合Promise/A+规范)

    Promise是前端面试中的高频问题,我作为面试官的时候,问Promise的概率超过90%,据我所知,大多数公司,都会问一些关于Promise的问题.如果你能根据PromiseA+的规范,写出符合规范 ...

  2. JS魔法堂: Native Promise Only源码剖析

    一, 前言 深入学习Promise的朋友应该都看过<深入理解Promise五部曲>这一系列的文章, 以解除回调地狱之外的观点来剖析Promise更多的内涵,确实十分精彩. Part 1: ...

  3. Promise的源码实现(符合Promise/A+规范)

    我们手写一个Promise/A+规范,然后安装测试脚本,以求通过这个规范. //Promise/A+源代码 // new Promise时,需要传递一个executor执行器,执行器立即执行 // e ...

  4. 异步解决方案promise及源码实现

    js语言的特性,造就了特别的异步处理方式,我记得以前用的最多的就是回调函数,那个时候写jquery的ajax时候,特别喜欢写这种代码: $.ajax({ method:'get', url:" ...

  5. promise源码解析

    前言 大部分同学对promise,可能还停留在会使用es6的promise,还没有深入学习.我们都知道promise内部通过reslove.reject来判断执行哪个函数,原型上面的then同样的,也 ...

  6. 从源码看 Promise 概念与实现

    Promise 是 JS 异步编程中的重要概念,它较好地解决了异步任务中回调嵌套的问题.在没有引入新的语言机制的前提下,这是如何实现的呢?上手 Promise 时常见若干晦涩的 API 与概念,它们又 ...

  7. 这一次,彻底理解Promise源码思想

    关于Promise的源码实现,网上有太多答案,我也看过很多资料,但都不是很明白.直到有一天我学完函数式编程之函子的概念,才对Promise源码有了更深刻的认识.今天,就让我们来重新认识一下Promis ...

  8. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

  9. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

随机推荐

  1. css---使用class和id

    网页现在的新标准是W3C.目前的模式是html+css+javascript,如何理解呢,就是html是网页的结构,CSS是网页的样式,javascript是行为.结构就是盖房子先要把结构建出来,然后 ...

  2. MC34063中文资料及应用实例(转)

    源:http://blog.chinaunix.net/uid-26199686-id-3207838.html MC34063A(MC33063)芯片器件简介 该器件本身包含了DC/DC变换器所需要 ...

  3. ural1613 For Fans of Statistics

    For Fans of Statistics Time limit: 1.0 secondMemory limit: 64 MB Have you ever thought about how man ...

  4. 【poj解题】1028

    stack的应用 #include<iostream> #include<stack> #include<stdio.h> #include<stdlib.h ...

  5. IO口输入输出模式理解

    1.IO输入输出模式 2.有上拉,下拉,弱上拉,推挽,开漏输出:不同的单片机有不同的输出模式 3.以最简单的51单片机为例 P0:开漏型双向IO口,通常需要添加外部上拉电阻 P1~P3:准双向IO口, ...

  6. C语言实现GBK/GB2312/五大码之间的转换(转)

    源:C语言实现GBK/GB2312/五大码之间的转换 //----------------------------------------------------------------------- ...

  7. javascript DOM 学习总结 (1)

    摘自javascript DOM编程艺术 1.首先介绍DOM的三个字母的含义: 1.1  D 如果没有document(文档),DOM 也无从谈起,当创建了一个网页并把他加载到web浏览器中时,DOM ...

  8. iOS8推送消息的快速回复处理

    http://blog.csdn.net/yujianxiang666/article/details/35260135 iOS8拥有了全新的通知中心,有全新的通知机制.当屏幕顶部收到推送时只需要往下 ...

  9. iOS开发——pch文件创建

    新换的公司,接手的项目里面连pch文件都没有,每次需要用到屏幕的宽高时,都是现写.今天既然碰到了,就把PCH这个玩意说一下. 1.Command+N,打开新建文件窗口:iOS->Other-&g ...

  10. JDBC连接数据库以及简单的操作

    package com.zhiyuan.jdbc.util; import java.sql.Connection;import java.sql.DriverManager;import java. ...