Promise 原理探究

前言:你真的了解Promise吗

你真的了解Promise吗?对我而言,除了知道如何使用then解决回调地狱以外,其他的还真的一知半解。虽然ES6的generator和ES7的async await提供了更先进的异步编程解决方案,但是它们还是离不开Promise,比如generator的co库的实现以及await后面必须返回promise。因此有必要深入了解一下Promise的原理。

看看下面这几道题,能不能轻松答出来?

假设 doSomething() 和 doSomethingElse() 返回一个 promise 对象,这些 promise 对象都代表了一个异步操作。那么:doSomething/doSomethingElse/finalHandler这三者的执行时序是怎样的?finalHandler分别会接受到什么值?出处:https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html

// ① 
doSomething().then(function () {
    return doSomethingElse();
}).then(finalHandler);

// ② 
doSomething().then(function () {
    doSomethingElse();
}).then(finalHandler);

// ③ 
doSomething().then(doSomethingElse()).then(finalHandler);

// ④ 
doSomething().then(doSomethingElse).then(finalHandler);

如果满脸黑人问号的话,下面我们通过实现一个简易的Promise——MyPromise来分析一下上面几道题。

ps. MyPromise仅实现核心功能,不保证完全满足promise规范。

一、雏形(v1)

Promise最基本的用法:调用resolve时,then的回调才会被执行,并得到resolve时的值。

new MyPromise(resolve => {
    resolve(2111);
}).then(val => {
    console.log(val); // 2111
});

1. 实现分析

从后往前看,首先MyPromise实例拥有then方法,而传入then的回调一定是晚于resolve执行的,因此这里通过闭包将then的回调存起来,等待被调用。

对于resolve而言,它的作用就是从闭包中取出then的回调进行调用,并透传参数值。

这里需要将callback的调用时机通过settimout放到下一事件循环中,让then方法先调用,否则会报TypeError: callback is not a function

function MyPromise(cb) {
    let callback = null;
    this.then = cb => { // 具有then方法
        callback = cb; // 调用`then`时,闭包保存回调
    }

    function resolve(val) {
        setTimeout(() => { // 下一事件循环再执行
            callback(val); // 取出闭包中的回调执行
        }, 0);
    }
	if (typeof cb == 'function') {
		cb(resolve); // 执行初始化cb,并传入resolve函数
	}
}

2. 问题

var pms = new MyPromise(function (resolve, reject) {
    resolve(2111);
});

// 异步调用then时
// TypeError: callback is not a function
setTimeout(function () {
	pms.then(function (val) {
	    console.log(val); 
	});
}, 100);

原因:resolvethen的调用顺序紊乱。

当resolve调用callback时,then的回调仍未被保存到callback中。

二、引入状态流转(v2)

通过状态流转,管理调用时序。这也是Promise规范规定的一个Promise有且仅有的三种状态之一:pending/fulfilled(resolved)/rejected (rejected这一版先不引入)

  • 状态初始值为pending
  • then早于resolve调用时:此时状态值仍是pending,因此可以保存onResolve回调,等待resolve调用
  • resolve早于then调用时:保存决议值,状态流转为resolved;等待then调用
function MyPromise(cb) {
    var cachedResolved = null;
    var state = 'pending';
    var resolvedVal;

    this.then = function (onResolved) {
        // 状态判断,pending时保存onResolved,否则直接调用onResolved
        if (state == 'pending') {
            cachedResolved = onResolved;
            return;
        }

        if (typeof onResolved == 'function') {
            onResolved(resolvedVal);
        }
    }

    function resolve(newVal) {
        resolvedVal = newVal; // 保存决议值
        state = 'resolved'; // 流转状态
		
        // 如then早被调用已保存了的回调,则可以直接执行
        if (typeof cachedResolved == 'function') {
            cachedResolved(resolvedVal);
        }
    }

    if (typeof cb == 'function') {
		cb(resolve);
	}
}

三、增加reject及错误处理(v3)

除了resolve函数能够流转状态之外,还有一个reject函数,调用后将会把当前promise的状态流转成rejected,用作异常处理。

1. 实现源码

function MyPromise(fn) {
	this.state = 'pending'; // 状态
    this.resolvedVal = ''; // 决议值
    this.rejectedReason = null; // 拒绝值
    this.onResolved = null; // resolve后的注册回调
    this.onRejected = null; // reject后的注册回调
	
	this.resolve = function (val) {
		try {
			// 每个promise的状态只能流转一次
			if (this.state != 'pending') {
				return;
			}
			this.state = 'resolved';
			this.resolvedVal = val;
			if (typeof this.onResolved == 'function') {
                console.log('triggered by resolve');
				this.onResolved(val);
			}
		} catch(err) {
			this.reject(err);
		}
	}
	
	this.reject = function (err) {
		if (this.state != 'pending') {
			return;
		}
		this.state = 'rejected';
		this.rejectedReason = err;
        
        if (typeof this.onRejected == 'function') {
            console.log('triggered by reject');
            this.onRejected(err);
        }
    }
    
    typeof fn == 'function' && fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.then = function (onResolved, onRejected) {
    // 未决议时 先存起来 等待决议
    if (this.state == 'pending') {
        this.onResolved = onResolved;
        this.onRejected = onRejected;
        return;
    }
    
    // 根据状态选择最终要调用的函数
    let finalCallback = this.state == 'resolved' ? onResolved : onRejected;

    if (typeof finalCallback == 'function') {
        console.log('triggered by then');
        finalCallback.call(this, this.value)
    }
};

2. 执行用例

new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(6666);
        reject('oops');
    }, 500);
}).then((val) => {
    console.log(val);  // 6666
}, (err) => {
    console.error(err); // 不会执行
});

3. 实现分析

对比v2,先看下then方法的改造:除了onResolved以外,新增了一个onRejected参数,用于处理reject状态的回调。

此时函数内根据三个不同状态,做出不同处理:

  • pending:此时状态仍未流转,因此分别缓存onResolvedonRejected,提供给resolvereject函数后续调用。
  • fulfilled(resolved):调用onResolved
  • rejected:调用onRejected
MyPromise.prototype.then = function (onResolved, onRejected) {
    // 未决议时 先存起来 等待决议
    if (this.state == 'pending') {
        this.onResolved = onResolved;
        this.onRejected = onRejected;
        return;
    }
    
    let finalCallback = this.state == 'resolved' ? onResolved : onRejected;

    if (typeof finalCallback == 'function') {
        console.log('triggered by then');
        finalCallback.call(this, this.value)
    }
};

reject函数与resolve的实现方式类似,状态流转成rejected后,调用onRejected函数。需要注意的是每个promise的状态只能流转一次,因此resolvereject中需要判断其状态,否则先后调用resolvereject函数(见上面的执行用例)会出现把promise的状态由resolved流转为rejected的诡异情况。

this.reject = function (err) {
	// 每个promise的状态只能流转一次
	if (this.state != 'pending') {
		return;
	}
    this.state = 'rejected'; // 流转状态
    this.rejectedReason = err; // 存储reject的值

    if (typeof this.onRejected == 'function') {
        console.log('triggered by reject');
        this.onRejected(err);
    }
}

同时在resolve中加入try catch捕获非预期错误后调用reject流转状态。

this.resolve = function (val) {
    try {
	    if (this.state != 'pending') {
			return;
		}
        this.state = 'resolved';
        this.resolvedVal = val;
        if (typeof this.onResolved == 'function') {
            console.log('triggered by resolve');
            this.onResolved(val);
        }
    } catch(err) {
        this.reject(err);
    }
}

四、简易Promise完整版(v4)

在完整版中,将加入以下的特性

  1. 支持then链式调用,每次调用then均返回一个新的promise
  2. 决议值为promise(非简单数值)以及 then返回promise时,需要反解出结果
  3. then未传入任何回调,此时应该透传上一个promise的结果

1. 实现源码

function MyPromise(fn) {
    this.state = 'pending'; // 状态
    this.resolvedVal = ''; // 决议值
    this.rejectedReason = null; // 拒绝值
    this.cached = null; 
	
	this.resolve = function (val) {
		try {
            if (this.state != 'pending') {
                return this.state == 'resolved' ? this.resolvedVal : this.rejectedReason;
            }

            // 如果决议值是一个promise或thenable对象(已决议或已拒绝),非数字字符串等,那么把结果反解出来
            if (typeof val == 'object' && typeof val.then == 'function') {
                val.then(this.resolve.bind(this), this.reject.bind(this));
                return;
            }

			this.state = 'resolved';
            this.resolvedVal = val;
            
			if (this.cached) {
				this.handler(this.cached);
			}
		} catch(err) {
			this.reject(err);
		}
	}
	
	this.reject = function (err) {
        if (this.state != 'pending') {
            return this.state == 'resolved' ? this.resolvedVal : this.rejectedReason;
        }

		this.state = 'rejected';
		this.rejectedReason = err;
        
        if (this.cached) {
            this.handler(this.cached);
        }
    }
    
    typeof fn == 'function' && fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.handler = function (opt = {}) {
    // 未决议时,缓存onResolved, onRejected, resolve, reject
    if (this.state == 'pending') {
        this.cached = opt;
        return;
    }

    // 存在then未传入任何回调的情况,这时应该透传上一个promise的结果 
    // new Promise(fn).then().then((val) => {console.log(val)})
    if (typeof opt.onResolved != 'function' && typeof opt.onRejected != 'function') {
        if (this.state == 'resolved') {
            opt.resolve(this.resolvedVal);
        } else {
            opt.reject(this.rejectedReason);
        }
        return;
    }

    try {
        let result;
        if (this.state == 'resolved') {
            // 执行resolved状态的回调
            result = opt.onResolved(this.resolvedVal);
            // 将回调的返回值作为新promise的决议值
            opt.resolve(result);
        } else {
            result = opt.onRejected(this.rejectedReason);
            opt.reject(result);
        }
    } catch (err) {
        opt.reject(err);
    }


};

MyPromise.prototype.then = function (onResolved, onRejected) {
    return new MyPromise((resolve, reject) => {
        this.handler({onResolved, onRejected, resolve, reject});
    });
};

2. 执行用例

new MyPromise((resolve, reject) => {
    resolve(100);
    // reject('error');
}).then((val) => {
    console.log(val);
    return val + 5;
}, (err) => {
    console.log(err);
}).then((val) => {
    console.log(val);

    return new MyPromise((resolve) => {
        resolve(200);
    });
}).then((val) => {
    val++;
    console.log(val);
}, (err) => {
    console.log(err);
});

3. 实现分析

(1)每次调用then均返回一个新的Promise

这一点除了用于支持链式调用以外,还很好地解决了一个Promise的状态只能流转一次的规定,因为调用resolvereject之后,这个Promise的生命周期就结束了。也就是说链式调用then,每次处理的都是一个全新的Promise。

Promise.prototype.then = function (onResolve, onReject) {
    return new Promise((resolve, reject) => {
	    // 通过handler函数统一处理resolve和reject逻辑
        this.handler({onResolve, onReject, resolve, reject});
    });
};

(2)反解内部的promise

上面几个版本的用例中,resolve接受的值以及then的返回值都是一个简单的字符或数字,如果类似下面,是一个promise的话,还需要p2和p3的值200和300解出来之后再作为决议值传给then

// example
new MyPromise((resolve, reject) => { // 父promise
	const p2 = new MyPromise((resolve, reject) => { // 子promise
		resolve(200);
	});
    resolve(p2);
}).then((val1) => { // then1
	console.log(val1 == 200) // true
	const p3 = new MyPromise((resolve, reject) => {
		resolve(300);
	});
	return p3;
}).then((val2) => { // then2
	console.log(val2 == 300) // true
});

实现这个特性,可以在resolve内增加以下逻辑:当接收的参数是一个promise(thenable对象)时,执行该promise的then,将父promise的resolvereject通过bind的方式绑定this后传入。目的是为了后面能够流转父promise的状态,若不流转状态的话(尝试将bind去掉),then1是不会被执行的。

this.resolve = function (val) {
	...
	if (typeof val == 'object' && typeof val.then == 'function') {
	   val.then(this.resolve.bind(this), this.reject.bind(this));
	   return;
	}
	
	this.state = 'resolved';
    this.resolvedVal = val;
    ...
}

(3) then未传入任何回调,透传上一promise决议值

new MyPromise(resovle => { // promise1
	resolve(123);
})
.then() // promise2
.then((val) => {
	console.log(val) // 123 
});

每次调用then都会返回一个新的promise,如果要第二个then被调用,则需要将第一个then返回的promise2的状态流转成resolved。因此在handler中增加相关的条件判断。

MyPromise.prototype.then = function (onResolved, onRejected) {
    return new MyPromise((resolve, reject) => {
	    // this是外层promise
	    // 调用resolve流转promise2的状态
        this.handler({onResolved, onRejected, resolve, reject});
    });
};

MyPromise.prototype.handler = function (opt = {}) {
	...
	
    if (typeof opt.onResolved != 'function' && typeof opt.onRejected != 'function') {
        if (this.state == 'resolved') {
	        // 流转promise2的状态,this.resolvedVal == 123
            opt.resolve(this.resolvedVal);
        } else {
            opt.reject(this.rejectedReason);
        }
        return;
    }
    
    ...
};

(4) 关于错误吞噬

下面的onRejected不会执行,这一点现在就比较好理解了。每个then都会返回新的promise,错误是发生在p2里面的,而onRejected捕获的是p1的错误。

new MyPromise(resolve => { // p1
	resolve(666);
}).then(throwError, onRejected); // p2

五、思考题答案

假设 doSomething() 和 doSomethingElse() 返回一个 promise 对象,这些 promise 对象都代表了一个异步操作。那么:finalHandler分别会接受到什么值?

① doSomething().then(function () {
    return doSomethingElse();
}).then(finalHandler);

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|


② doSomething().then(function () {
    doSomethingElse();
}).then(finalHandler);
doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|
           
                  
③ doSomething().then(doSomethingElse()).then(finalHandler);
doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
                  finalHandler(resultOfDoSomething)
                  |------------------|


④ doSomething().then(doSomethingElse).then(finalHandler);
doSomething
|-----------------|
                  doSomethingElse(resultOfDoSomething)
                  |------------------|
                                     finalHandler(resultOfDoSomethingElse)
                                     |------------------|

上面四道题的重点其实大致可以归到第四章分析里的·这三点

  1. 每次调用then均返回一个新的Promise
  2. 反解内部的promise
  3. then未传入任何回调,透传上一promise决议值

第一题

为什么finalHandler的执行顺序在doSomethingElse之后?

finalHandler是then2的onResolve回调,等待的是then1生成的promise。而then1生成的promise的决议值是doSomethingElse()的返回值。

第二题

为什么doSomethingElse和finalHandler几乎同时执行?

doSomethingElse()并没有作为then1的返回值,因此then1生成的promise会立即流转状态为resolved,决议值为undefined,决议之后finalHandler作为then2的onResolve回调,会立即执行。

第三题

doSomethingElse()返回值是一个promise,不能作为then1的onResolve回调,因此这种情况相当于then未传入任何回调,这时会将doSomething的决议值透传到then2的finalHandler中去。

第四题

这是正常的用法,doSomethingElse作为then1的onResolve回调,接收doSomething()的决议值,执行后返回另一个promise,then1会将这个promise解开并将其决议值作为他的promise的决议值,因此等待then1生成的promise的finalHandler就可以获取到resultOfDoSomethingElse。

参考文章

http://www.mattgreer.org/articles/promises-in-wicked-detail/ https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html http://liubin.org/promises-book/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA高级架构

面试中单例模式有几种写法

“你知道茴香豆的‘茴’字有几种写法吗?” 纠结单例模式有几种写法有用吗?有点用,面试中经常选择其中一种或几种写法作为话头,考查设计模式和coding style...

2606
来自专栏人工智能LeadAI

TensorFlow中的多线程

TensorFlow提供两个类帮助实现多线程,一个是tf.train.Coordinator,另一个是tf.train.QueueRunner。Coordina...

3477
来自专栏Golang语言社区

golang基于redis lua封装的优先级去重队列

前言: 前两天由于某几个厂商的api出问题,导致后台任务大量堆积,又因为我这边任务流系统会重试超时任务,所以导致队列中有大量的重复任务。这时候我们要临时解决两个...

3519
来自专栏向治洪

Java中的ReentrantLock和synchronized两种锁机制的对比

原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什...

1955
来自专栏nummy

Tornado入门(二)【异步和阻塞IO】

实时Web应用通常针对每个用户创建持久连接,对于传统的同步服务器,这意味着需要给每个用户单独创建一个线程,这样做的代价非常高。

662
来自专栏非典型技术宅

Swift多线程:GCD进阶,单例、信号量、任务组1. dispatch_once,以及Swift下的单例2. dispatch_after3. 队列的循环、挂起、恢复4. 信号量(semaphore

1715
来自专栏腾讯NEXT学位

Nodejs探秘:深入理解单线程实现高并发原理

为什么单线程的nodejs可以支持高并发呢?很多人都不明白其原理,自己也在很长一段时间内被这些概念搞的是云里雾里。下面我们就来一步一步揭开其神秘的面纱。

4242
来自专栏Golang语言社区

golang基于redis lua封装的优先级去重队列

前言: 前两天由于某几个厂商的api出问题,导致后台任务大量堆积,又因为我这边任务流系统会重试超时任务,所以导致队列中有大量的重复任务。这时候我们要临时解决两个...

33111
来自专栏移动端开发

Android学习--探究服务(一)

      服务(service)是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务。服务的运行不依赖任...

741
来自专栏编程

Python学习笔记1——斐波那契数列

这是一个高中同学问我的问题,本来是用C来写的,正好正在学Python,就用Python重写了一遍当作练习。 下面是题目要求: ? ? 一道很简单的题目,但有些细...

19410

扫码关注云+社区