手写promise,了解一下(二)

184 阅读5分钟

支持链式操作

我们平时写promise一般都是对应的一组流程化的操作,如这样: promise.then(f1).then(f2).then(f3) 但是我们之前的版本最多只能注册一个回调,这一节我们就来实现链式操作。

目标

使promise支持链式操作

实现

想支持链式操作,其实很简单,首先存储回调时要改为使用数组

self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];

当然执行回调时,也要改成遍历回调数组执行回调函数

self.onFulfilledCallbacks.forEach((callback) => callback(self.value));

最后,then方法也要改一下,只需要在最后一行加一个return this即可,这其实和jQuery链式操作的原理一致,每次调用完方法都返回自身实例,后面的方法也是实例的方法,所以可以继续执行。

Promise.prototype.then = function(onFulfilled, onRejected){
	let self = this;

	if(self.state === 'resolved'){
		onFulfilled(self.value);
	}

	if(self.state === 'rejected'){
		onRejected(self.reson);
	}

	if(self.state === 'padding'){
		self.onResolvedCallbacks.push(function(){
			onFulfilled(self.value);
		});
		self.onRejectedCallbacks.push(function(){
			onRejected(self.reson);
		});
	}
	
	return this;
}

这种简单返回this会有一个问题,这里为方便讲解我们引入一个常见场景:用promise顺序读取文件内容,场景代码如下:

let p = new Promise((resolve, reject) => {
    fs.readFile('../file/1.txt', "utf8", function(err, data) {
        err ? reject(err) : resolve(data)
    });
});
let f1 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/2.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f2 = function(data) {
    console.log(data)
    return new Promise((resolve, reject) => {
        fs.readFile('../file/3.txt', "utf8", function(err, data) {
            err ? reject(err) : resolve(data)
        });
    });
}
let f3 = function(data) {
    console.log(data);
}
let errorLog = function(error) {
    console.log(error)
}
p.then(f1).then(f2).then(f3).catch(errorLog)

上面场景,我们读取完1.txt后并打印1.txt内容,再去读取2.txt并打印2.txt内容,再去读取3.txt并打印3.txt内容,而读取文件都是异步操作,所以都是返回一个promise,我们上一节实现的promise可以实现执行完异步操作后执行后续回调,但是本节的回调读取文件内容操作并不是同步的,而是异步的,所以当读取完1.txt后,执行它回调onFulfilledCallbacks里面的f1,f2,f3时,异步操作还没有完成,所以我们本想得到这样的输出:

this is 1.txt
this is 2.txt
this is 3.txt

但是实际上却会输出

this is 1.txt
this is 1.txt
this is 1.txt

所以要想实现异步操作串行,我们不能将回调函数都注册在初始promise的onFulfilledCallbacks里面,而要将每个回调函数注册在对应的异步操作promise的onFulfilledCallbacks里面,用读取文件的场景来举例,f1要在p的onFulfilledCallbacks里面,而f2应该在f1里面return的那个Promise的onFulfilledCallbacks里面,因为只有这样才能实现读取完2.txt后才去打印2.txt的结果。

但是,我们平常写promise一般都是这样写的: promise.then(f1).then(f2).then(f3),一开始所有流程我们就指定好了,而不是在f1里面才去注册f1的回调,f2里面才去注册f2的回调。

如何既能保持这种链式写法的同时又能使异步操作衔接执行呢?我们其实让then方法最后不再返回自身实例,而是返回一个新的promise即可,我们可以叫它bridgePromise,它最大的作用就是衔接后续操作,我们看下具体实现代码:

function Promise(executor){
	let self = this;
	self.state = 'padding';
	self.value = undefined;
	self.reason = undefined;
	self.onResolvedCallbacks = [];
	self.onRejectedCallbacks = [];

	function resolve(value){
		if(self.state == 'padding'){
			self.state = 'resolved'
			self.value = value
			self.onResolvedCallbacks.forEach(fn=>fn());
		}	
	}

	function reject(reason){
		if(self.state == 'padding'){
			self.state = 'rejected'
			self.reason = reason
			self.onRejectedCallbacks.forEach(fn=>fn());
		}
	}

	try{
		executor(resolve, reject);
	}catch(e){
		reject(e);
	}
}

Promise.prototype.then = function(onFulfilled, onRejected){
	let self = this;
	//不能写成:let promise2 = new Promise(function(resolve, reject){}}
	let promise2;
	promise2 = new Promise(function(resolve, reject){
		if(self.state === 'resolved'){
			try{
				let x = onFulfilled(self.value);
				resolvePromise(promise2, x, resolve, reject);
			}catch(e){
				reject(e);
			}
		}

		if(self.state === 'rejected'){
			try{
				let x = onRejected(self.reason);
				resolvePromise(promise2, x, resolve, reject);
			}catch(e){
				reject(e);
			}
		}

		if(self.state === 'padding'){
			self.onResolvedCallbacks.push(function(){
				try{
					let x = onFulfilled(self.value);
					resolvePromise(promise2, x, resolve, reject);
				}catch(e){
					reject(e);
				}
			});
			self.onRejectedCallbacks.push(function(){
				try{
					let x = onRejected(self.reason);
					resolvePromise(promise2, x, resolve, reject);
				}catch(e){
					reject(e);
				}
			});
		}
	});

	return promise2;	
}


/**
 * [resolvePromise description]
 * @param  {[type]} promise2 [then的返回值(新的promise)]
 * @param  {[type]} x        [then种成功或失败的返回值]
 * @param  {[type]} resolve  [promise2中的resolve]
 * @param  {[type]} reject   [promise2中的reject]
 * @return {[type]}          [description]
 */
function resolvePromise(promise2, x, resolve, reject){
	//console.log('resolvePromise');
	if(promise2 === x){
		return reject(new TypeError('循环引用promise'));
	}

	if(x!==null && (typeof x === 'object' || typeof x === 'function')){
		let then = x.then;
		if(typeof then === 'function'){//说明返回的是promise,只有promise上才有then
			console.log('function');
			then.call(
				x, 
				y=>resolve(y), 
				err=>reject(err)
			);
		}else{//说明返回的是 普通对象
			console.log('object');
			resolve(x);
		}
	}else{//说明返回的是 普通值
		resolve(x);
	}
}

这里很抽象,我们还是以文件顺序读取的场景画一张图解释一下流程:

当执行p.then(f1).then(f2).then(f3)时:

  1. 先执行p.then(f1)返回了一个bridgePromise(p2),并在p的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f1并且更新p2的状态.
  2. 然后.then(f2)时返回了一个bridgePromise(p3),这里注意其实是p2.then(f2),因为p.then(f1)时返回了p2。此时在p2的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f2并且更新p3的状态.
  3. 然后.then(f3)时返回了一个bridgePromise(p4),并在p3的onFulfilledCallbacks回调列表中放入一个回调函数,回调函数负责执行f3并且更新p4的状态. 到此,回调关系注册完了,如图所示:

  1. 然后过了一段时间,p里面的异步操作执行完了,读取到了1.txt的内容,开始执行p的回调函数,回调函数执行f1,打印出1.txt的内容“this is 1.txt”,并将f1的返回值放到resolvePromise中开始解析。resolvePromise一看传入了一个promise对象,promise是异步的啊,得等着呢,于是就在这个promise对象的then方法中继续resolvePromise这个promise对象resolve的结果,一看不是promise对象了,而是一个具体值“this is 2.txt”,于是调用bridgePromise(p2)的reslove方法将bridgePromise(p2)的状态更新为fulfilled,并将“this is 2.txt”传入p2的回调函数中去执行。

  2. p2的回调开始执行,f2拿到传过来的“this is 2.txt”参数开始执行,打印出2.txt的内容,并将f2的返回值放到resolvePromise中开始解析,resolvePromise一看传入了一个promise对象,promise是异步的啊,又得等着呢........后续操作就是不断的重复4,5步直到结束。