promise/bluebird源码
本作品采用知识共享署名 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源码的更多相关文章
- Promise的源码实现(完美符合Promise/A+规范)
Promise是前端面试中的高频问题,我作为面试官的时候,问Promise的概率超过90%,据我所知,大多数公司,都会问一些关于Promise的问题.如果你能根据PromiseA+的规范,写出符合规范 ...
- JS魔法堂: Native Promise Only源码剖析
一, 前言 深入学习Promise的朋友应该都看过<深入理解Promise五部曲>这一系列的文章, 以解除回调地狱之外的观点来剖析Promise更多的内涵,确实十分精彩. Part 1: ...
- Promise的源码实现(符合Promise/A+规范)
我们手写一个Promise/A+规范,然后安装测试脚本,以求通过这个规范. //Promise/A+源代码 // new Promise时,需要传递一个executor执行器,执行器立即执行 // e ...
- 异步解决方案promise及源码实现
js语言的特性,造就了特别的异步处理方式,我记得以前用的最多的就是回调函数,那个时候写jquery的ajax时候,特别喜欢写这种代码: $.ajax({ method:'get', url:" ...
- promise源码解析
前言 大部分同学对promise,可能还停留在会使用es6的promise,还没有深入学习.我们都知道promise内部通过reslove.reject来判断执行哪个函数,原型上面的then同样的,也 ...
- 从源码看 Promise 概念与实现
Promise 是 JS 异步编程中的重要概念,它较好地解决了异步任务中回调嵌套的问题.在没有引入新的语言机制的前提下,这是如何实现的呢?上手 Promise 时常见若干晦涩的 API 与概念,它们又 ...
- 这一次,彻底理解Promise源码思想
关于Promise的源码实现,网上有太多答案,我也看过很多资料,但都不是很明白.直到有一天我学完函数式编程之函子的概念,才对Promise源码有了更深刻的认识.今天,就让我们来重新认识一下Promis ...
- Netty 源码解析(三): Netty 的 Future 和 Promise
今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...
- jQuery之Deferred源码剖析
一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...
随机推荐
- css---使用class和id
网页现在的新标准是W3C.目前的模式是html+css+javascript,如何理解呢,就是html是网页的结构,CSS是网页的样式,javascript是行为.结构就是盖房子先要把结构建出来,然后 ...
- MC34063中文资料及应用实例(转)
源:http://blog.chinaunix.net/uid-26199686-id-3207838.html MC34063A(MC33063)芯片器件简介 该器件本身包含了DC/DC变换器所需要 ...
- ural1613 For Fans of Statistics
For Fans of Statistics Time limit: 1.0 secondMemory limit: 64 MB Have you ever thought about how man ...
- 【poj解题】1028
stack的应用 #include<iostream> #include<stack> #include<stdio.h> #include<stdlib.h ...
- IO口输入输出模式理解
1.IO输入输出模式 2.有上拉,下拉,弱上拉,推挽,开漏输出:不同的单片机有不同的输出模式 3.以最简单的51单片机为例 P0:开漏型双向IO口,通常需要添加外部上拉电阻 P1~P3:准双向IO口, ...
- C语言实现GBK/GB2312/五大码之间的转换(转)
源:C语言实现GBK/GB2312/五大码之间的转换 //----------------------------------------------------------------------- ...
- javascript DOM 学习总结 (1)
摘自javascript DOM编程艺术 1.首先介绍DOM的三个字母的含义: 1.1 D 如果没有document(文档),DOM 也无从谈起,当创建了一个网页并把他加载到web浏览器中时,DOM ...
- iOS8推送消息的快速回复处理
http://blog.csdn.net/yujianxiang666/article/details/35260135 iOS8拥有了全新的通知中心,有全新的通知机制.当屏幕顶部收到推送时只需要往下 ...
- iOS开发——pch文件创建
新换的公司,接手的项目里面连pch文件都没有,每次需要用到屏幕的宽高时,都是现写.今天既然碰到了,就把PCH这个玩意说一下. 1.Command+N,打开新建文件窗口:iOS->Other-&g ...
- JDBC连接数据库以及简单的操作
package com.zhiyuan.jdbc.util; import java.sql.Connection;import java.sql.DriverManager;import java. ...