Promise原理详解
从入门Promise的正确姿势中我们已经了解到Promise的基本用法。那么现在给你一个需求:根据Promise的用法和Promise/A+规范,实现一个自己的Promise函数。
1、Promise构造函数的结构
根据Promise的用法我们知道:在new一个Promise函数的时候,Promise函数必须接受一个函数作为参数。我们暂且把这个参数函数称为执行器。
执行器提供两个参数(resolve,reject),且内部有状态机制(pending,resolved,rejected)。Promise构造函数的原型上有then方法。
我们再来看一下PromiseA+标准中是怎样规定的:
- Promise对象初始状态为
Pending
,在被resolve
或reject
时,状态变为Fulfilled
或Rejected
- resolve接收成功的数据,reject接收失败或错误的数据
- Promise对象必须有一个
then
方法,且只接受两个函数参数onFulfilled
、onRejected
由以上标准就容易就能实现这个类的大致结构。
开始撸:
var PENDING = 'pending',
RESOLVED = 'resolved',
REJECTED = 'rejected',
function Promise(executor) {
//养成良好的编程习惯,先对参数做一个容错处理
if (executor && typeof executor != 'function') {
throw new Error('Promise is not a function');
}
let _this = this;//缓存this
_this.status = PENDING;//当前Promise的状态
_this.value = undefined;//Promise成功执行时要传递给回调的数据,默认为undefined
_this.reason = undefined;//Promise失败执行时传递给毁掉的原因,默认为undefined
function resolve(value) { //内置一个resolve方法
if (_this.status == PENDING) {
_this.status = RESOLVED;//当调用resolve时,将Promise的状态改为resoled
_this.value = value;//保存成功调用时传递进来的数据
}
}
function reject(reason) {
if (_this.status == PENDING) {
_this.status = REJECTED;//当调用reject时,将Promise的状态改为rejected
_this.reason = reason;//保存失败调用时传递进来的原因
}
}
executor(resolve,reject); }
//then方法可以接受两个函数参数,分别表示当前Promise执行成功时的调用onFulfilled和执行失败时调用onRejected
Promise.prototype.then = function(onFulfilled,onRejected) {
let _this = this,//缓存this,保不齐后面会用到,当然如果你不想缓存this,也可以在后面使用箭头函数
if (_this.status == RESOLVED) {
onFulfilled(_this.value);
}
if (_this.status == REJECTED) {
onFulfilled(_this.reason);
}
}
看起来不错,但回调函数是立即执行的,也就是说上面实现的Promise只支持同步代码,而无法进行异步操作,比如这样是不行的
let p = new Promise(function(resolve, reject){
setTimeout(function(){
resolve('成功执行了!');
}, 1000)
})
p.then(function(data){
console.log('成功', data)
},function(err){
console.log('失败', err)
})
// 不会输出任何代码
原因是:我们在then函数中只对成功态和失败态进行了判断,而实例被new时,执行器中的代码会立即执行,但setTimeout中的代码将稍后执行,也就是说,then方法执行时,Promise的状态没有被改变依然是pending态。
所以我们要对pending态也做判断,而由于代码可能是异步的,所以回调函数就不该被立即执行,所以我们就要想办法把回调函数进行缓存,当状态改变后(由pending态变成resolved或rejected态),再执行相应的函数。
那么状态在什么时候改变呢:很显然,在执行resolve或rejected函数的时候,状态会发生改变。
并且,then方法是可以多次使用的,所以要能存多个回调,那么这里我们用一个数组来存储多个回调函数。
1.1、实现异步
在实例上挂载两个参数
_this.onResolvedCallbacks = []; // 存放then成功的回调
_this.onRejectedCallbacks = []; // 存放then失败的回调
我们再给then方法加一个pending时的判断
if(_this.status === 'pending'){
// 每一次then时,如果是等待态,就把回调函数push进数组中,什么时候改变状态什么时候再执行
_this.onResolvedCallbacks.push(function(){ // 这里用一个函数包起来,是为了后面加入新的逻辑进去
onFulfilled(_this.value)
})
_this.onRejectedCallbacks.push(function(){ // 同理
onRjected(_this.reason)
})
}
下一步要分别在resolve和reject方法里加入执行数组中存放的函数的方法,修改一下上面的resolve和reject方法
function resolve(value) {
if (_this.status === 'pending') {
_this.status = 'resolved'
_this.value = value
_this.onResolvedCallbacks.forEach(function(fn){ // 当成功的函数被调用时,之前缓存的回调函数会被一一调用
fn()
})
}
}
function reject(reason) {
if (_this.status === 'pending') {
_this.status = 'rejected'
_this.reason = reason
_this.onRejectedCallbacks.forEach(function(fn){// 当失败的函数被调用时,之前缓存的回调函数会被一一调用
fn()
})
}
}
现在可以执行异步任务了,也可以给一个Promise函数多次then了。
1.2、错误处理
上面的代码看似很完美,前提是整个Promise函数能够正确执行,那样就没reject函数什么事了。但我们必须要考虑的一种情况是:当Promise函数执行出错时怎么办,所以一个健壮的Promise函数是应该有错误处理机制的。所以我们应该在Promise的代码体中加入try catch,如果出现异常,则捕捉错误交给reject。
我们实现一下,思路很简单,在执行器执行时进行try catch
try{
executor(resolve, reject)
}catch(e){ // 如果捕获发生异常,直接调失败,并把参数穿进去
reject(e)
}
2、实现then的链式调用(难点)
上面说过了,then可以链式调用,也是这一点让Promise十分好用,当然这部分源码也比较复杂
我们知道jQuery实现链式调用是return了一个this,但Promise不行,为什么不行?
因为then函数内返回的是一个新的Promise对象。我们看一下标准是如何定义的:
标准中规定:
then
方法必须返回一个新的Promise实例
(ES6中的标准,Promise/A+中没有明确说明)- 为了保证
then
中回调的执行顺序,onFulfilled
或onRejected
必须异步调用
所以我们要做的就是:在then方法中先定义一个新的Promise,取名为promise2,然后在三种状态下分别用promise2包装一下,在调用onFulfilled时用一个变量x接收返回值,try catch一下代码,没错就调resolve传入x,有错就调reject传入错误,最后再把promise2给return出去,就可以进行链式调用了
//修改then
Promise.prototype.then = function(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) {return vaule};
onRejected = typeof onRejected === 'function' ? onFulfilled : function(err) {throw err}; let _this = this;//缓存this,保不齐后面会用到,当然如果你不想缓存this,也可以在后面使用箭头函数
let promise2;
if (_this.status === RESOLVED) {
promise2 = new Promise(function(resolve,reject) {
try {
//onFulfilled函数的执行情况要考虑多种情况,后面会细说
let x = onFulfilled(_this.value);//将执行onFulfilled的返回值传给x,这里需要注意的是执行过程中有可能会出错
resolve(x);
} catch(e) {
reject(e);
}
}) }
if (_this.status === REJECTED) {
promise2 = new Promise(function(resolve,reject) {
try {
//onFulfilled函数的执行情况要考虑多种情况,后面会细说
let x = onRejected(_this.reason);//将执行onFulfilled的返回值传给x(即这次then函数执行的返回值),这里需要注意的是执行过程中有可能会出错
resolve(x);//这个x会被下一次的then函数接收到
} catch(e) {
reject(e);
}
}) } if(_this.status === PENDING){
promise2 = new Promise(function(resolve,reject) {
// 每一次then时,如果是等待态,就把回调函数push进数组中,什么时候改变状态什么时候再执行
_this.onResolvedCallbacks.push(function(){ // 这里用一个函数包起来,是为了后面加入新的逻辑进去
try {
let x = onFulfilled(_this.value);
resolve(x);
} catch(e) {
reject(e);
}
})
_this.onRejectedCallbacks.push(function(){ // 同理
try {
let x = onRejected(_this.reason);
resolve(x);
} catch(e) {
reject(e);
}
})
}) }
}
上面的实现虽然能用,但是很粗糙。
在接下的分析之前,我希望大家能够清晰的明白x的值代表什么:x表示的是上一次的then函数或promise函数执行结果的返回值,这个x的值会被resolve(x),作为下一次then函数调用时的参数。
明确了x的值以后,接下来对onFulfilled和onRejected函数可能出现的情况做一个列举,这也是为了我们的Promise函数能做到最大的容错率:
- 前一次then返回一个普通值,字符串数组对象这些东西,都没问题,只需传给下一个then,刚才的方法就够用。
- 前一次then返回的是一个Promise,是正常的操作,也是Promise提供的语法糖,我们要想办法判断到底返回的是啥。
- 前一次then返回的是一个Promise,其中有异步操作,也是理所当然的,那我们就要等待他的状态改变,再进行下面的处理。
- 前一次then返回的是自己本身这个Promise
var p1 = p.then(function(){// 这里得用var,let由于作用域的原因会报错undefined
return p1
})
5.前一次then返回的是一个别人自己随便写的Promise,这个Promise可能是个有then的普通对象,比如{then:'哈哈哈'},也有可能在then里故意抛错(这种蛋疼的 操作我们也要考虑进去)。比如他这样写
let promise = {}
Object.defineProperty(promise,'then',{
value: function(){
throw new Error('报错气死你')
}
})
// 如果返回这东西,我们再去调then方法就肯定会报错了
6.调resolve的时候再传一个Promise下去,我们还得处理这个Promise。
p.then(function(data) {
return new Promise(function(resolve, reject) {
resolve(new Promise(function(resolve,reject){
resolve(1111)
}))
})
})
7.可能既调resolve又调reject,得忽略后一个。
8.光then,里面啥也不写。
问题8最好解决,我们只要手动添加一个onFulfilled和onRejected进去就好了。
对于问题1-7,我们可以采取统一的觉得方案,定义一个函数来判断和处理这一系列的情况,官方给出了一个叫做resolvePromise的函数。
所以,我们进一步来完善then方法。
Promise.prototype.then = function(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) {return value};
onRejected = typeof onRejected === 'function' ? onRejected : function(err) {throw err};
//这里正好解释了在正式的Promise函数中,我们为什么可以不写onRejected函数,
//因为then方法内部会帮我们封装好一个onRejected函数,用来抛出上一次then或Promise执行出错的信息,这也是为什么可以在最后执行catch方法的原因 let _this = this;//缓存this,保不齐后面会用到,当然如果你不想缓存this,也可以在后面使用箭头函数
let promise2;
if (_this.status === RESOLVED) {
promise2 = new Promise(function(resolve,reject) {
try {
//onFulfilled函数的执行情况要考虑多种情况,后面会细说
let x = onFulfilled(_this.value);//将执行onFulfilled的返回值传给x,这里需要注意的是执行过程中有可能会出错
resolvePromise(promise2,x,resolve,reject);
} catch(e) {
reject(e);
}
}) }
if (_this.status === REJECTED) {
promise2 = new Promise(function(resolve,reject) {
try {
//onFulfilled函数的执行情况要考虑多种情况,后面会细说
let x = onRejected(_this.reason);//将执行onFulfilled的返回值传给x(即这次then函数执行的返回值),这里需要注意的是执行过程中有可能会出错
resolvePromise(promise2,x,resolve,reject);//这个x会被下一次的then函数接收到
} catch(e) {
reject(e);
}
}) } if(_this.status === PENDING){
promise2 = new Promise(function(resolve,reject) {
// 每一次then时,如果是等待态,就把回调函数push进数组中,什么时候改变状态什么时候再执行
_this.onResolvedCallbacks.push(function(){ // 这里用一个函数包起来,是为了后面加入新的逻辑进去
try {
let x = onFulfilled(_this.value);
resolvePromise(promise2,x,resolve,reject);
} catch(e) {
reject(e);
}
})
_this.onRejectedCallbacks.push(function(){ // 同理
try {
let x = onRejected(_this.reason);
resolvePromise(promise2,x,resolve,reject);
} catch(e) {
reject(e);
}
})
}) }
return promise2;
}
定义resolvePromise方法:
function resolvePromise(promise2, x, resolve, reject) {
// 接受四个参数: 新的Promise、返回值,成功和失败的回调
// 有可能这里返回的x是别人的promise
// 尽可能允许其他乱写
if (promise2 === x) { //这里应该报一个类型错误,来解决问题4
return reject(new TypeError('循环引用了'))
}
// 看x是不是一个promise,promise应该是一个对象
let called = false; // 表示是否调用过成功或者失败,用来解决问题7
//下面判断上一次then返回的是普通值还是函数,来解决问题1、2
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 可能是promise {},看这个对象中是否有then方法,如果有then我就认为他是promise了
try {
let then = x.then;// 保存一下x的then方法
if (typeof then === 'function') {
// 成功
//用call方法修改指针为x,否则this指向window
then.call(x, function (y) {//如果x是一个Promise对象,y参数表示x执行后的resolve值
//console.log(y);
if (called) return //如果调用过就return掉
called = true
// y可能还是一个promise,在去解析直到返回的是一个普通值
resolvePromise(promise2, y, resolve, reject)//递归调用,解决了问题6
}, function (err) { //失败时执行的函数
if (called) return
called = true
console.log('r');
reject(err);
})
} else {//如果x不是一个Promise对象,则直接resolve(x)
resolve(x)
}
} catch (e) {
if (called) return
called = true;
reject(e);
}
} else { // 说明是一个普通值
resolve(x); // 表示成功了
}
}
2.1、catch方法
由于catch方法是then(null, onRejected)的语法糖,所以这里也很好实现。
Promise.prototype.catch = function(onRejected) {
return this.then(null,onRejected);
}
至此,一个简易粗糙的Promise函数已经实现了。同学们可以自己用自己写的这个Promise函数验证一下是否是否可行。
Promise原理详解的更多相关文章
- es6的promise用法详解
es6的promise用法详解 promise 原理 promise是es6的异步编程解决方案, 是es6封装好的对象: 一个promise有三种状态:Pending(进行中).Resolved(已完 ...
- I2C 基础原理详解
今天来学习下I2C通信~ I2C(Inter-Intergrated Circuit)指的是 IC(Intergrated Circuit)之间的(Inter) 通信方式.如上图所以有很多的周边设备都 ...
- Zigbee组网原理详解
Zigbee组网原理详解 来源:互联网 作者:佚名2015年08月13日 15:57 [导读] 组建一个完整的zigbee网状网络包括两个步骤:网络初始化.节点加入网络.其中节点加入网络又包括两个 ...
- 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解
CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...
- SSL/TLS 原理详解
本文大部分整理自网络,相关文章请见文后参考. SSL/TLS作为一种互联网安全加密技术,原理较为复杂,枯燥而无味,我也是试图理解之后重新整理,尽量做到层次清晰.正文开始. 1. SSL/TLS概览 1 ...
- 锁之“轻量级锁”原理详解(Lightweight Locking)
大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意. 原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖 ...
- [转]js中几种实用的跨域方法原理详解
转自:js中几种实用的跨域方法原理详解 - 无双 - 博客园 // // 这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同 ...
- 节点地址的函数list_entry()原理详解
本节中,我们继续讲解,在linux2.4内核下,如果通过一些列函数从路径名找到目标节点. 3.3.1)接下来查看chached_lookup()的代码(namei.c) [path_walk()> ...
- WebActivator的实现原理详解
WebActivator的实现原理详解 文章内容 上篇文章,我们分析如何动态注册HttpModule的实现,本篇我们来分析一下通过上篇代码原理实现的WebActivator类库,WebActivato ...
随机推荐
- 购买小米成功 散分mhn
之前,我们都没办法解决近来的寡言,面对越来越少的语言,只能是憋得气闷.却没想后来这么一件事可以解开那个尴尬. 前两天晚上,他被我一次次闪躲去看小说伤害,难受,却还是不想和我争吵什么.后来,偶然的机会让 ...
- c#设计模式之装饰器模式(Decorator Pattern)
引子 在面向对象语言中,我们常常会听到这样一句话:组合优于继承.那么该如何去理解这句话呢? 下面我将以游戏装备为模型用简单的代码去展示它 先创建一个装备的抽象类,然后创建刀枪2个具体的业务子类 pub ...
- SQL 增加列
-- 判断是否存在该列 if exists(SELECT * FROM syscolumns WHERE id=object_id('@Table') and name='@COLUMN') ALTE ...
- mysql--MySQL数据库的简单认识
一.MySQL介绍 1.mysql版本 双授权版本:社区版(完全免费,功能也够nb了)和商业版(更好,功能更多更强大一些,但是收费,VIP,有售后服务,也会参考和吸收社区版的一些nb的功能,安全性和稳 ...
- 12c ocp 062新考题(之前没出现过)-1
1.One of your databases has archive logging enabled and RMAN backups are taken at regular intervals. ...
- jquery优化轮播图2
继续优化 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...
- 洛谷P2664 树上游戏(点分治)
传送门 题解 因为一个sb错误调了一个晚上……鬼晓得我为什么$solve(rt)$会写成$solve(v)$啊!!!一个$O(logn)$被我硬生生写成$O(n)$了竟然还能过$5$个点……话说还一直 ...
- 3D-2D:PnP
PnP(Perspective-n-Point):当知道n个3D空间点及其投影位置时,估计相机位姿. 2D-2D的对极几何方法需要八个或八个以上的点对(以八点法为例),且存在着初始化.纯旋转和尺度的问 ...
- Spring Boot启动过程(六):内嵌Tomcat中StandardHost、StandardContext和StandardWrapper的启动
看代码有助于线上出现预料之外的事的时候,不至于心慌... StandardEngine[Tomcat].StandardHost[localhost]的启动与StandardEngine不在同一个线程 ...
- js的语言的理解
1.所谓字面量,就是语言语法 2.在js编译器读到语法时候,执行时候创建对象:在赋值的时候创建一个对象,或者是一个匿名对象. 3.函数定义本身是一个对象:执行时候不产生实例对象:这跟python类不一 ...