小而美的Promise库——promiz源码浅析
背景
在上一篇博客[[译]前端基础知识储备——Promise/A+规范](https://segmentfault.com/a/11...,我们介绍了Promise/A+规范的具体条目。在本文中,我们来选择了promiz,让大家来看下一个具体的Promise库的内部代码是如何运作的。
promiz是一个体积很小的promise库(官方介绍约为913 bytes (gzip)),作为一个ES2015标准中的Promise的polyfill,实现了诸如resolve、all和race等API。
知识储备
我们在这里简单回顾一下Promise/A+的主要关键点,如果需要了解详细内容的同学,可以阅读我的上一篇博客。
- Promise有三个状态,分别为
pending、fulfilled和rejected,且只能从pending到fulfilled或者rejected,没有其他的流转方式。 - Promise的返回值是一个新的Promise,原因见上一条。
- 传递给
then函数的两个回调函数,有且仅有一次机会被执行(即执行了onfulfilled就不会执行onrejected函数,且只执行一次)。
代码实现与分析
异步执行器
在介绍Promise之前,我们先介绍一下异步执行器。在Promise中,我们需要一个异步的执行器来异步执行我们的回调函数。在规范中提到,通常情况下,我们可以使用微任务(nextTick)或者宏任务(setTimeout)来实现。但是,如果我们需要兼容Web Worker这种情况的话,我们可能还需要一些更多的方式来处理。具体代码如下:
var queueId = 1
var queue = {}
var isRunningTask = false
// 使用postMessage来执行异步函数
if (!global.setImmediate)
global.addEventListener('message', function (e) {
if (e.source == global) {
if (isRunningTask)
nextTick(queue[e.data])
else {
isRunningTask = true
try {
queue[e.data]()
} catch (e) {}
delete queue[e.data]
isRunningTask = false
}
}
})
/**
* 异步执行方法
* @param {function} fn 需要执行的回调函数
*/
function nextTick(fn) {
if (global.setImmediate) setImmediate(fn)
// 如果在Web Worker中使用以下方法
else if (global.importScripts) setTimeout(fn)
else {
queueId++
queue[queueId] = fn
global.postMessage(queueId, '*')
}
}
以上代码比较简单,我们简单说明下:
在代码中,promiz使用了
setImmediate、setTimeout和postMessage这三个方法来执行异步函数,其中:setImmedeate,只有IE实现了该方法,在执行完队列中的代码后立即执行。PostMessage,新增的H5中的方法。setTimeout,兼容性最佳,可以适用各种场景。
因此,在promiz的这段代码中,有一定的兼容性问题,应该把setTimeout放到最后作为一个兜底策略,否则无法在老浏览器中执行。
构造函数
说完了异步函数执行器,我们来看下promise的构造函数。
首先我们来看下内存数据,我们需要存储当前promise的状态、成功的值或者失败的原因、下一个promise的引用和成功与失败的回调函数。因此,我们需要以下变量:
// states
// 0: pending
// 1: resolving
// 2: rejecting
// 3: resolved
// 4: rejected
var self = this,
state = 0, // promise状态
val = 0, // success callback返回值
next = [], // 返回的新的promise对象
fn, er; // then方法中的成功回调函数和失败回调函数
在存储完相关数据后,我们来看下构造函数。
function Deferred(resolver) {
...
self = this;
try {
if (typeof resolver == 'function')
resolver(self['resolve'], self['reject'])
} catch (e) {
self['reject'](e)
}
}
构造函数非常简单,除了声明相关的函数,就只有执行传入的callback而已。当然,如果我们不是链式调用的第一个promise,那么我们会没有resolver参数,因此不需要在此执行,我们会在then函数执行resolve方法。
下面我们来看下上面提到的处理函数resovle和reject。
self['resolve'] = function (v) {
fn = self.fn
er = self.er
if (!state) {
val = v
state = 1
nextTick(fire)
}
return self
}
self['reject'] = function (v) {
fn = self.fn
er = self.er
if (!state) {
val = v
state = 2
nextTick(fire)
}
return self
}
self['then'] = function (_fn, _er) {
if (!(this._d == 1))
throw TypeError()
var d = new Deferred()
d.fn = _fn
d.er = _er
if (state == 3) {
d.resolve(val)
}
else if (state == 4) {
d.reject(val)
}
else {
next.push(d)
}
return d
}
在resolve和reject这两个函数中,都是改变了内部promise的状态,给定了参数值,同时异步触发了fire函数。而then方法,则是生成了一个新的Deferred对象,并且完成了相关的初始化(执行完then方法我们就会得到这个新生成的Deferred对象,也就是一个新的Promise);当前一个promise到达resolved状态时,不需要等待则直接出发resolve方法,rejected状态时也一样。那么,让我们来看下fire方法到底是做什么的呢?
function fire() {
// 检测是不是一个thenable对象
var ref;
try {
ref = val && val.then
} catch (e) {
val = e
state = 2
return fire()
}
thennable(ref, function () {
state = 1
fire()
}, function () {
state = 2
fire()
}, function () {
try {
if (state == 1 && typeof fn == 'function') {
val = fn(val)
}
else if (state == 2 && typeof er == 'function') {
val = er(val)
state = 1
}
} catch (e) {
val = e
return finish()
}
if (val == self) {
val = TypeError()
finish()
} else thennable(ref, function () {
finish(3)
}, finish, function () {
finish(state == 1 && 3)
})
})
}
从上面的代码来看,fire函数只是判断了ref是不是一个thenable对象,然后调用了thenable函数,传递了3个回调函数。那么这些回调函数到底是做什么用的呢?我们需要来看下thenable函数的实现代码。
// ref:指向thenable对象的`then`函数
// cb, ec, cn : successCallback, failureCallback, notThennableCallback
function thennable(ref, cb, ec, cn) {
// Promises can be rejected with other promises, which should pass through
if (state == 2) {
return cn()
}
if ((typeof val == 'object' || typeof val == 'function') && typeof ref == 'function') {
try {
// cnt变量用来保证成功和失败的回调函数总共只会被执行一次
var cnt = 0
ref.call(val, function (v) {
if (cnt++) return
val = v
cb()
}, function (v) {
if (cnt++) return
val = v
ec()
})
} catch (e) {
val = e
ec()
}
} else {
cn()
}
};
在thenable函数中,如果判断当前的promise的状态是处于rejecting时,会直接执行cn,也就是将reject状态传递下去。而如果当ref不是一个thenable对象的then函数时(那么此时值为undefined),那么就会直接执行cn。
通过fire函数传递的三个callback我们可以看到,cn是在promise的状态改变时,针对特定的状态来触发相对应的onfulfilled或者onrejected回调函数。
只有当ref是一个thenable时(传递给resolve的是一个promise),代码才会进入上面的try catch逻辑中。
Promise执行流程
看完了上面的各部分代码,我相信大家可能对整个执行流程仍然不够熟悉,下面,我们将这些流程拼接起来,通过几个完整的流程来说明下。
链式调用第一个Promise
当我们声明一个promise式,我们会传入一个resolver。此时,整个Deferred对象的state是0。如果我们在resolver里面调用了resolve方法,那么我们的state就会变成1,然后出发fire函数注册到thenable函数里面的第三个回调函数,从而将值传递给下一个thenable。当thenable的then函数执行完成(即我们看到的Promise后面跟着的then函数执行完成以后),我们的state才会变成3,也就是说上一个Promise才会结束,返回一个新的Promise。
链式调用非第一个Promise
如果不是第一个Promise,那么我们就没有resolver参数。因此,我们的resolve方法并不是通过在resolver中进行调用的,而是将回调函数fn注册进来,在上一个Promise完成后主动调用执行的。也就是说,我们在上一个Promise执行完then函数并且返回一个新的Promise时,我们这个返回的Promise就已经进入了resolving的状态。
给resolve传递一个Promise
在Promise/A+规范中,如果我们给resolve传递一个promise,那么我们的通过resolve获取到的值就是传递进去的这个promise返回的值。当然,我们也必须等待作为参数的这个promise处理完成后,才会处理外面的这个promise。
在promiz的代码中,我们如果通过resolve接收到一个promise,那么我们在fire函数中就会吧promise.then的引用传递给thenable函数。在thenable函数中,我们会将我们当前promise需要执行的onfulfilled和onrejected封装成一个函数,传递给作为参数的promise的then函数。因此,当作为参数的promise执行任意结果的回调函数时,就会将参数传递给外层的promise,执行对应的回调函数。
全局执行方法
Promise.all
让我们先看代码。
Deferred.all = function (arr) {
if (!(this._d == 1))
throw TypeError()
if (!(arr instanceof Array))
return Deferred.reject(TypeError())
var d = new Deferred()
function done(e, v) {
if (v)
return d.resolve(v)
if (e)
return d.reject(e)
var unresolved = arr.reduce(function (cnt, v) {
if (v && v.then)
return cnt + 1
return cnt
}, 0)
if (unresolved == 0)
d.resolve(arr)
arr.map(function (v, i) {
if (v && v.then)
v.then(function (r) {
arr[i] = r
done()
return r
}, done)
})
}
done()
return d
}
在Promise.all中,我们使用了一个计数器来进行统计,在每一个Promise后面都增加一个then函数用于增加计数。当Promise成功时则计数+1。当整个数组中的Promise都已经进入resolved状态时,我们才会执行thenable的then函数。如果有一个失败的话,则立即进入reject流程。
总结
从代码设计层面来看,promiz的代码量较少,阅读也较为简单。但是,在某些细节的设计上,promiz还是体现出了较为巧妙的思路,如在处理作为入参的promise时,能够在这个promise后面动态的添加一个then函数,从而获取数据给外面的promise。
如果大家有兴趣,建议自己根据本文的说明阅读一遍源码,配合Promise/A+规范来看下是如何实现每一条规范的。
下一篇博客,我们将为大家从头开始,来实现一个Promise库。
原文地址:https://segmentfault.com/a/1190000016880176
小而美的Promise库——promiz源码浅析的更多相关文章
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Android源码浅析(三)——Android AOSP 5.1.1源码的同步sync和编译make,搭建Samba服务器进行更便捷的烧录刷机
Android源码浅析(三)--Android AOSP 5.1.1源码的同步sync和编译make,搭建Samba服务器进行更便捷的烧录刷机 最近比较忙,而且又要维护自己的博客,视频和公众号,也就没 ...
- redux 源码浅析
redux 源码浅析 redux 版本号: "redux": "4.0.5" redux 作为一个十分常用的状态容器库, 大家都应该见识过, 他很小巧, 只有 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Struts2源码浅析-ConfigurationProvider
ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...
- (转)【深入浅出jQuery】源码浅析2--奇技淫巧
[深入浅出jQuery]源码浅析2--奇技淫巧 http://www.cnblogs.com/coco1s/p/5303041.html
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- Android 手势识别类 ( 三 ) GestureDetector 源码浅析
前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...
- Android开发之Theme、Style探索及源码浅析
1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...
随机推荐
- poj 1186 方程的解数【折半dfs+hash】
折半搜索,map会T所以用hash表来存状态 #include<iostream> #include<cstdio> #include<map> using nam ...
- Java面试必刷常见真题200+ ,让你“过五关,斩六将”,轻松入大厂
这份面试清单是我从 2015 年做 TeamLeader 之后开始收集的,一方面是给公司招聘用,另一方面是想用它来挖掘我在 Java 技术栈中的技术盲点,然后修复和完善它,以此来提高自己的技术水平.虽 ...
- NSA互联网公开情报收集指南:迷宫中的秘密·上
猫宁!!! 参考链接: https://www.nsa.gov/news-features/declassified-documents/assets/files/Untangling-the-Web ...
- selenium自动化测试实例
11.1使用JavaScripExecutor单击元素 被测试网页:http://www.sogou.com Java代码 public class NewTest { WebDriver ...
- SQL 实战语句(9)
1.如何同步根据部门表同步另外一个项目表的部门字段 update A set A.auditownerid=(select lp.epleaderId from (select ep.userid a ...
- [NOIP2018校模拟赛]T2矩阵分组 Matrix
题目链接: 矩阵分组 分析: 这道题求的是两部分极差当中大的那个的最小值.对于这种求最值的问题,我们很自然(其实并没有)地想到二分答案. 这个题有两个结论: (好像当时看出来了第一个?然后发现下面都不 ...
- 转:从《The C Programming Language》中学到的那些编程风格和设计思想
这儿有一篇写的很好的读后感:http://www.cnblogs.com/xkfz007/articles/2566424.html 读书不是目的,关键在于思考. 很早就在水木上看到有人推荐& ...
- C8051特点
C8051与传统51的区别在于优先权交叉开关.系统时钟.SFR寄存器几个方面: 一 优先权交叉开关:传统的51外设功能是固定分配或者复用分配到指定引脚,而C8051则是通过优先权交叉开关设置,即要想分 ...
- JVM线程与Linux内核线程的映射[转]
Linux从内核2.6开始使用NPTL (Native POSIX Thread Library)支持,但这时线程本质上还轻量级进程. Java里的线程是由JVM来管理的,它如何对应到操作系统的线程是 ...
- poj1190 生日蛋糕
题意: 要制作一个体积为Nπ的M层生日蛋糕,每层都是一个圆柱体. 设从下往上数第i(1 <= i <= M)层蛋糕是半径为Ri, 高度为Hi的圆柱.当i < M时,要求Ri > ...