深入理解Promise运行原理

深入理解Promise运行原理

大家好,又见面了,我是全栈君。

本文大多数内容翻译自该篇文章

1.什么是Promise

Promise可以认为是一种用来解决异步处理的代码规范。常见的异步处理是使用回调函数,回调函数有两种模式,同步的回调和异步的回调。一般回调函数指的是异步的回调。

同步回调

	function add(a, b, callback) { callback(a + b) }
        
        console.log('before');
        add(1, 2, result => console.log('Result: ' + result);
        console.log('after');
复制代码

输出结果为: before Result:3 after

异步回调

    function addAsync(a, b, callback) {
        setTimeout( () => callback(a + b), 1000);
    }

    console.log('before');
    addAsync(1, 2, result => console.log('Result: ' + result));
    console.log('after');
复制代码

输出结果: before after Result: 3

然而回调函数有个著名的坑就是“callback hell”,比如:

	doSomething1(function(value1) {
		doSomething2(function(value2) {
			doSomething3(function(value3) {
				console.log("done! The values are: " + [value1, value2, value3].join(','));
			})
		})
	})
复制代码

为了等value1, value2, value3数据都准备好,必须要一层一层嵌套回调函数。如果一直嵌套下去,就形成了callback hell,不利于代码的阅读。

如果改用Promise的写法,只要写成如下方式就行。

	doSomething1().then(function() {
		return value1;
	}).then(function(tempValue1) {
		return [tempValue1, value2].join(',');		
	}).then(function(tempValue2) {
		console.log("done! ", [tempValue2, value3].join(','));
	});
复制代码

可以注意到,Promise实际上是把回调函数从doSomething函数中提取到了后面的then方法里面,从而防止多重嵌套的问题。

一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。它要么解析成功,要么失败抛出异常。它允许你以一种同步的方式编写异步代码。

Promise的实现是根据Promises/A+规范实现的。

2.Promise对象和状态

对于Promise的基本使用和入门,可以参考promise-book。这里对Promise的使用做了比较详细的介绍。

2.1 resolve & reject

Promise构造函数用来构造一个Promise对象,其中入参匿名函数中resolvereject这两个也都是函数。如果resolve执行了,则触发promise.then中成功的回调函数;如果reject执行了,则触发promise.then中拒绝的回调函数。

	var promise = new Promise(function(resolve, reject) {
		// IF 如果符合预期条件,调用resolve
		resolve('success');

		// ELSE 如果不符合预期条件,调用reject
		reject('failure')
	})
复制代码

2.2 Fulfilled & Rejected

Promise对象一开始的值是Pending准备状态。

执行了resolve()后,该Promise对象的状态值变为onFulfilled状态。

执行了reject()后,该Promise对象的状态值变为onRejected状态。

Promise对象的状态值一旦确定(onFulfilled或onRejected),就不会再改变。即不会从onFulfilled转为onRejected,或者从onRejected转为onFulfilled。

2.3 快捷方法

获取一个onFulfilled状态的Promise对象:

Promise.resolve(1);

// 等价于

new Promise((resolve) => resolve(1));
复制代码

获取一个onRejected状态的Promise对象:

Promise.reject(new Error("BOOM")) 

// 等价于

new Promise((resolve, reject) 
	=> reject(new Error("BOOM")));
复制代码

更多快捷方法请参考Promise API

3.异常捕获:then和catch

Promise的异常捕获有两种方式:

  1. then匿名函数中的reject方法
  2. catch方法

3.1 then中的reject方法捕获异常

这种方法只能捕获前一个Promise对象中的异常,即调用then函数的Promise对象中出现的异常。

	var promise = Promise.resolve();

	promise.then(function() {
	    throw new Error("BOOM!")
	}).then(function (success) {
	    console.log(success);
	}, function (error) {
		// 捕捉的是第一个then返回的Promise对象的错误
	    console.log(error);
	});
复制代码

但该种方法无法捕捉当前Promise对象的异常,如:

	var promise = Promise.resolve();

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}, function (error) {
	    console.log(error);  // 无法捕捉当前then中抛出的异常
	});
复制代码

3.2 catch捕获异常

上述栗子若改写成如下形式,最后追加一个catch函数,则可以正常捕捉到异常。

	var promise = Promise.resolve();

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).catch(function (error) {
        console.log(error); // 可以正常捕捉到异常
    });
复制代码

catch方法可以捕获到then中抛出的错误,也能捕获前面Promise抛出的错误。 因此建议都通过catch方法捕捉异常。

	var promise = Promise.reject("BOOM!");

	promise.then(function() {
	    return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).catch(function (error) {
        console.log(error);  // BOOM!
    });
复制代码

值得注意的是:catch方法其实等价于then(null, reject),上面可以写成:

	promise.then(function() {
		return 'success';
	}).then(function (success) {
	    console.log(success);
		throw new Error("Another BOOM!");
	}).then(null, function(error) {
		console.log(error);
	})
复制代码

总结来说就是:

  1. 使用promise.then(onFulfilled, onRejected)的话,在 onFulfilled中发生异常的话,在onRejected中是捕获不到这个异常的。

  2. promise.then(onFulfilled).catch(onRejected)的情况下then中产生的异常能在.catch中捕获

  3. .then.catch在本质上是没有区别的需要分场合使用。

4.动手逐步实现Promise

了解一个东西最好的方式就是尝试自己实现它,尽管可能很多地方不完整,但对理解内在的运行原理是很有帮助的。

这里主要引用了JavaScript Promises … In Wicked Detail这篇文章的实现,以下内容主要是对该篇文章的翻译。

4.1 初步实现

首先实现一个简单的Promise对象类型。只包含最基本的then方法和resolve方法,reject方法暂时不考虑。

function Promise(fn) {
	// 设置回调函数
	var callback = null;

	// 设置then方法
	this.then = function (cb) {
		callback = cb;
	};

	// 定义resolve方法
	function resolve(value) {
		// 这里强制resolve的执行在下一个Event Loop中执行
		// 即在调用了then方法后设置完callback函数,不然callback为null
		setTimeout(function () {
			callback(value);
		}, 1);
	}

	// 运行new Promise时传入的函数,入参是resolve
	// 按照之前讲述的,传入的匿名函数有两个方法,resolve和reject
	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

// 调用自己的Promise
doSomething().then(function (value) {
	console.log("got a value", value);
});
复制代码

好了,这是一个很粗略版的Promise。这个实现连Promise需要的三种状态都还没实现。这个版本主要直观展示了Promise的核心方法:thenresolve

该版本如果then异步调用的话,还是会导致Promise中的callback为null。

	var promise = doSomething();
	
	setTimeout(function() {
	    promise.then(function(value) {
	    	console.log("got a value", value);
	})}, 1);
复制代码

后续通过加入状态来维护Promise,就可以解决这种问题。

4.2 Promise添加状态

通过添加一个字段state用来维护Promise的状态,当执行了resolve函数后,修改stateresolved,初始statependding

function Promise(fn) {

	var state = 'pending'; // 维护Promise实例的状态
	var value;
	var deferred; // 在状态还处于pending时用于保存回调函数的引用

	function resolve(newValue) {
		value = newValue;
		state = 'resolved';

		if (deferred) {
			// deferred 有值表明回调已经设置了,调用handle方法处理回调函数
			handle(deferred);
		}
	}

	// handle方法通过判断state选择如何执行回调函数
	function handle(onResolved) {
		// 如果还处于pending状态,则先保存then传入的回调函数
		if (state === 'pending') {
			deferred = onResolved;
			return;
		}

		onResolved(value);
	}

	this.then = function (onResolved) {
		// 对then传入的回调函数,调用handle去执行回调函数
		handle(onResolved);
	};

	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

doSomething().then(function (value) {
	console.log("got a value", value);
});
复制代码

加入了状态后,可以通过判断状态来解决调用先后顺序的问题:

  • resolve()执行前调用then()。表明这时还没有value处理好,这时的状态就是pending,此时先保留then()传入的回调函数,等调用resolve()处理好value值后再执行回调函数,此时回调函数保存在deferred中。

  • resolve()执行后调用then()。表明这时value已经通过resolve()处理完成了。当调用then()时就可以通过调用传入的回调函数处理value值。

该版本的Promise我们可以随意先调用resolve()pending(),两者的顺序对程序的执行不会造成影响了。

4.3 Promise添加调用链

Promise是可以链式调用的,每次调用then()后都返回一个新的Promise实例,因此要修改之前实现的then()方法。

function Promise(fn) {
	var state = 'pending';
	var value;
	var deferred = null;

	function resolve(newValue) {
		value = newValue;
		state = 'resolved';

		if (deferred) {
			handle(deferred);
		}
	}

	// 此时传入的参数是一个对象
	function handle(handler) {
		if (state === 'pending') {
			deferred = handler;
			return;
		}

		// 如果then没有传入回调函数
		// 则直接执行resolve解析value值
		if (!handler.onResolved) {
			handler.resolve(value);
			return;
		}

		// 获取前一个then回调函数中的解析值
		var ret = handler.onResolved(value);
		handler.resolve(ret);
	}

	// 返回一个新的Promise实例
	// 该实例匿名函数中执行handle方法,该方法传入一个对象
	// 包含了传入的回调函数和resolve方法的引用
	this.then = function (onResolved) {
		return new Promise(function (resolve) {
			handle({
				onResolved: onResolved, // 引用上一个Promise实例then传入的回调
				resolve: resolve
			});
		});
	};

	fn(resolve);
}

function doSomething() {
	return new Promise(function (resolve) {
		var value = 42;
		resolve(value);
	});
}

// 第一个then的返回值作为第二个then匿名函数的入参
doSomething().then(function (firstResult) {
	console.log("first result", firstResult);
	return 88;
}).then(function (secondResult) {
	console.log("second result", secondResult);
});
复制代码

then中是否传入回调函数也是可选的,如:

doSomething().then().then(function(result) {
  	console.log('got a result', result);
});
复制代码

handle()方法的实现中,如果没有回调函数,直接解析已有的value值,该值是上一个Promise实例中调用resolve(value)中传入的。

if(!handler.onResolved) {
  	handler.resolve(value);
  	return;
}
复制代码

如果回调函数中返回的是一个Promise对象而不是一个具体数值怎么办?此时我们需要对返回的Promise调用then()方法。

	doSomething().then(function(result) {
	  	// doSomethingElse returns a promise
	  	return doSomethingElse(result);
	}).then(function(anotherPromise) {
	  	anotherPromise.then(function(finalResult) {
	    	console.log("the final result is", finalResult);
	  	});
	});
复制代码

每次这样写很麻烦,我们可以在我们的Promise中的resole()方法内处理掉这种情况。

function resolve(newValue) {
	// 通过判断是否有then方法判断其是否是Promise对象
	if (newValue && typeof newValue.then === 'function') {
		// 递归执行resolve方法直至解析出值出来, 
		// 通过handler.onResolved(value)解析出值,这里handler.onResolve就是resolve方法
		newValue.then(resolve);
		return;
	}

	state = 'resolved';
	value = newValue;

	if (deferred) {
		handle(deferred);
	}
}
复制代码

4.4 Promise添加reject处理

直至目前为止,已经有了一个比较像样的Promise了,现在添加一开始忽略的reject()方法,使得我们可以这样使用Promise。

doSomething().then(function(value) {
  	console.log('Success!', value);
}, function(error) {
  	console.log('Uh oh', error);
});
复制代码

实现也很简单,reject()方法与resolve()方法类似。

function Promise(fn) {
	var state = 'pending';
	var value;
	var deferred = null;

	function resolve(newValue) {
		if (newValue && typeof newValue.then === 'function') {
			newValue.then(resolve, reject);
			return;
		}
		state = 'resolved';
		value = newValue;

		if (deferred) {
			handle(deferred);
		}
	}

	// 添加的reject方法,这里将Promise实例的状态设为rejected
	function reject(reason) {
		state = 'rejected';
		value = reason;

		if (deferred) {
			handle(deferred);
		}
	}

	function handle(handler) {
		if (state === 'pending') {
			deferred = handler;
			return;
		}

		var handlerCallback;

		// 添加state对于rejected状态的判断
		if (state === 'resolved') {
			handlerCallback = handler.onResolved;
		} else {
			handlerCallback = handler.onRejected;
		}

		if (!handlerCallback) {
			if (state === 'resolved') {
				handler.resolve(value);
			} else {
				handler.reject(value);
			}

			return;
		}

		var ret = handlerCallback(value);
		handler.resolve(ret);
	}

	this.then = function (onResolved, onRejected) {
		return new Promise(function (resolve, reject) {
			handle({
				onResolved: onResolved,
				onRejected: onRejected,
				resolve: resolve,
				reject: reject
			});
		});
	};

	fn(resolve, reject);
}



function doSomething() {
	return new Promise(function (resolve, reject) {
		var reason = "uh oh, something bad happened";
		reject(reason);
	});
}

// 调用栗子
doSomething().then(function (firstResult) {
	// wont get in here
	console.log("first result:", firstResult);
}, function (error) {
	console.log("got an error:", error);
});
复制代码

目前我们的异常处理机制只能处理自己抛出的异常信息,对于其他的一些异常信息是无法正常捕获的,如在resolve()方法中抛出的异常。我们对此做如下修改:

	function resolve(newValue) {
	  	try {
	    	// ... as before
	  	} catch(e) {
	    	     reject(e);
	  	}
	}
复制代码

这里通过添加try catch手动捕获可能出现的异常,并在catch中调用reject()方法进行处理。同样对于回调函数,执行时也可能出现异常,也需要做同样的处理。

	function handle(deferred) {
	  	// ... as before
	
	  	var ret;
	  	try {
	    	    ret = handlerCallback(value);
	  	} catch(e) {
	    	    handler.reject(e);
	    	    return;
	  	}
	
	  	handler.resolve(ret);
	}
复制代码

上述完整的演示代码请查看原文作者提供的fiddle

4.4 Promise保证异步处理

到目前为止,我们的Promise已经实现了基本比较完善的功能了。这里还有一点需要注意的是,Promise规范提出不管是resolve()还是reject(),执行都必须保持异步处理。要实现这一点很简单,只需做如下修改即可:

	function handle(handler) {
	  	if(state === 'pending') {
	    	    deferred = handler;
	    	    return;
	  	}

	  	setTimeout(function() {
	    	    // ... as before
	  	}, 1);
	}
复制代码

问题是为什么要这么处理?这主要是为了保证代码执行流程的一致性和可靠性。考虑如下栗子:

	var promise = doAnOperation();
	invokeSomething();
	promise.then(wrapItAllUp);
	invokeSomethingElse();
复制代码

通过代码的意图应该是希望invokeSomething()invokeSomethingElse()都执行完后,再执行回调函数wrapItAllUp()。如果Promise的resolve()处理不是异步的话,则执行顺序变为invokeSomething() -> wrapItAllUp() -> invokeSomethingElse(),跟预想的产生不一致。

为了保证这种执行顺序的一致性,Promise规范要求resolve必须是异步处理的。

到这一步,我们的Promise基本像模像样了。当然离真正的Promise还有一段差距,比如缺乏了常用的便捷方法如all(),race()等。不过本例子实现的方法本来就是从理解Promise原理出发的,相信通过该例子对Promise原理会有比较深入的了解。

参考

  1. JavaScript Promises … In Wicked Detail
  2. Promises/A+
  3. promise-book
  4. A quick guide to JavaScript Promises
  5. MDN web docs
  6. Node.js Design Patterns
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/107752.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 避免在移动端页面中使用100vh

    避免在移动端页面中使用100vh100vh带来的问题在CSS中,视口单位(Viewportunits)听起来不错。如果要设置一个元素的样式使它占据整个屏幕的高度,那么你可以设置height:100vh,这样你就拥有一个完美的全屏元素,该元素会随着视口的变化而调整大小!可惜的是,事实并非如此。100vh在移动浏览器中以一种微妙但基本的方式被破坏,使其几乎无用。最好避免使用100vh,而应该通过javascript设置高度的方…

  • Java并发面试_常见面试题

    Java并发面试_常见面试题预览并发问题详解请谈谈你对volatile的理解linkCAS你知道吗?link原子类Atomiclntegerl的ABA问题谈谈?原子更新引用知道吗?link我们知道ArrayList是线程不安全,请编码写一个不安全的案例并给出解决方案link公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁linkCountDownLatch/CyclicBarrier/Semaphore使用过吗?link阻塞队列知道吗?l

  • 医学图形图像处理(医学影像和医学图像处理)

    文章目录1图像和数字图像1图像和数字图像  数字图像:被定义为一个二维函数,f(x,y),其中x,y代表空间坐标,f代表点(x,y)处的强度或灰度级。和普通的笛卡尔坐标系有区别,在计算机中坐标系左上角为原点:  图像数字化:图像进入计算机后,对图像进行数字化(映射)。数字图像三要素:  (1)像素:大小决定了图像存储、显示的清晰度;  (2)灰度值:通常为0-255,因为在计算机中通常用一个字节来表示一个像素,即28。  (3)坐标  图像存储在计算机中会丢失信息,因为是从一个连续的

  • netty 释放bytebuf_python高性能框架

    netty 释放bytebuf_python高性能框架目录一、ByteBuf介绍二、分配方式堆缓冲区直接缓冲区ByteBufAllocatorUnpooled缓冲区三、ByteBuf的操作可丢弃字节可读字节可写字节索引管理查找操作派生缓冲区引用计数工具类资源释放一、ByteBuf介绍网络数据的基本单位总是字节。JavaNIO提供了ByteBuffer作为它的字节容器…

  • java 数组转化为list_java中如何将数组转为list集合?

    java 数组转化为list_java中如何将数组转为list集合?java中将数组转为list集合的方法:1、使用原生方式,使用for()循环来拆分数组,并添加到List中;2、使用Arrays.asList()方法;3、使用Collections.addAll()方法;4、使用List.of()方法。问题描述:对于给定的如下数组,如何转换成List集合?String[]array={“a”,”b”,”c”};参考stackoverflow总结如下几种写法…

  • Laravel引入第三方库的方法

    Laravel引入第三方库的方法

    2021年10月25日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号