一、JavaScript Promise

1、简介

Promise是一个ES6提供的类,目的是更加优雅地书写复杂的异步任务。

由于Promise是ES6新增的,所以一些旧的浏览器并不支持,苹果的Safari 10和Windows的Edge 14版本以上的浏览器才支持,这个需要注意。

2、构造Promise

新构建第一个Promise对象:
     new Promise(function(resolve, reject) {

// 处理的逻辑

});

    通过新建一个Promise对象好像并没有看出它怎么实现“更加优雅地书写复杂的异步任务”。接下来,我们通过需要多次调用异步函数来看下:

普通方式:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Javascript基础学习</title>
	</head>
	<body>
		<h2>JavaScript Promise</h2>
		<button onclick="yibu()">异步</button>
		<p id="one"></p>
		<p id="two"></p>
		<p id="three"></p>
	</body>

	<script>
		function yibu() {
			setTimeout(function() {
				document.getElementById("one").innerHTML = "3000ms -- First";
				console.log("First");
				setTimeout(function() {
				    document.getElementById("two").innerHTML = "4000ms -- Second";
					console.log("Second");
					setTimeout(function() {
				        document.getElementById("three").innerHTML = "1000ms -- Third";
						console.log("Third");
					}, 3000);
				}, 4000);
			}, 1000);
		}
	</script>
</html>

输出结果:

第七节JavaScript Promise_开发语言

从上面也能看出,这种“函数瀑布”实现的无论是维护还是异常处理都是特别繁琐的事情,而且也会让缩进格式变得冗赘。

下面我们用Promise来实现同样的功能:

代码:

<script>
		function yibu() {
			new Promise(function(resolve, reject) {
				setTimeout(function() {
					document.getElementById("one").innerHTML = "3000ms -- First";
					console.log("First");
					resolve();
				}, 3000);

			}).then(function() {
				return new Promise(function(resolve, reject) {
					setTimeout(function() {
						document.getElementById("two").innerHTML = "4000ms -- Second";
						console.log("Second");
						resolve();
					}, 4000);
				});

			}).then(function() {
				setTimeout(function() {
					document.getElementById("three").innerHTML = "1000ms -- Third";
					console.log("Third");
				}, 1000);
			});
		}
	</script>

输出结果:

第七节JavaScript Promise_构造函数_02

这段代码也是很长的,现在我们不需要完全理解它,代码引起我们注意的是Promise将嵌套格式的代码变成了顺序格式的代码。

3、Promise 的构造函数

Promise构造函数是JavaScript中用于创建Promise对象的内置构造函数。

Promise构造函数接受一个函数作为参数,该函数是同步的并且立即执行,所以我们称之为起始函数。起始函数包含两个参数 resolve和reject,分别表示Promise成功和失败的状态。

起始函数执行成功时,它应该调用resolve函数并传递成功的结果。当起始函数执行失败时,它应该调用reject函数并传递失败的原因。

Promise构造函数返回一个Promise对象,该对象具有以下几个方法:

  • then:用于处理Promise成功状态的回调函数
  • catch:用于处理Promise失败状态的回调函数
  • finally:无论Promise是成功还是失败,都会执行的回到函数。

        下面实例是使用Promise构造含税创建Promise对象(当Promise被构造时,起始函数会被同步执行):

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Javascript基础学习</title>
	</head>
	<body>
		<h2>JavaScript Promise</h2>
		<button onclick="yibu()">异步</button>
		<p id="one"></p>
		<p id="two"></p>
		<p id="three"></p>
	</body>

	<script>
		function yibu() {
			const prpmise01 = new Promise(function(resolve, reject) {
				setTimeout(function() {
					document.getElementById("one").innerHTML = "3000ms -- First";
					console.log("First");
					resolve("success First");
				}, 3000);

			})

			prpmise01.then(function(result) {
				document.getElementById("two").innerHTML = result;
			})
			
			const prpmise02 = new Promise(function(resolve, reject) {
				setTimeout(function() {
					console.log("prpmise02");
					reject("error three First");
				}, 3000);
			
			})
			
			prpmise02.catch(function(error) {
				document.getElementById("three").innerHTML = error;
			})
		}
	</script>
</html>

输入结果:

第七节JavaScript Promise_开发语言_03

        在上面例子中,我们使用Promise构造函数创建了两个Promise对象,并使用setTimeout模拟一个异步场景。第一个异步操作成功,则会调用resolve函数并传递成功的结果;第二个异步场景失败,则调用reject函数并传递失败的原因。然后,我们使用then方法处理Promise成功状态的回调函数,使用catch方法处理Promise失败状态的回调函数。

接下来我们再看个小示例:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Javascript基础学习</title>
	</head>
	<body>
		<h2>JavaScript Promise</h2>
		<button onclick="yibu()">计算(a / b 或 a / c)</button>
		<p id="one"></p>
		<p id="two"></p>
	</body>

	<script>
		function yibu() {
			new Promise(function(resolve, reject) {
				var a = 4;
				var b = 2;
				if (b == 0) {
					reject("a / b The denominator cannot be zero")
				} else {
					var value = a / b;
					resolve(value);
				}
			}).then(function(result) {
				document.getElementById("one").innerHTML = "a / b = " + result;
			}).catch(function(error) {
				document.getElementById("two").innerHTML = "a / c error = " + error;
			})

			new Promise(function(resolve, reject) {
				var a = 4;
				var c = 0;
				if (c == 0) {
					reject("a / c - The denominator cannot be zero")
				} else {
					var value = a / c;
					resolve(value);
				}
			}).then(function(result) {
				document.getElementById("one").innerHTML = "a / c = " + result;
			}).catch(function(error) {
				document.getElementById("two").innerHTML = "a / c error = " + error;
			}).finally(function(){
			document.getElementById("three").innerHTML = "End";
			})
		}
	</script>
</html>

输出结果:

第七节JavaScript Promise_javascript_04

        说明:Promise类有.then()、.catch()和.finally()三个方法,这三个方法的参数都是一个函数,.then()可以将参数中的函数添加到当前Promise的正常执行序列,.catch()则是设定的异常处理序列,.finally()是在Promise执行的最后一定会执行的序列。.then()传入的函数会按照顺序依次执行,在有任何异常都会直接跳转到catch序列。

例如:

第七节JavaScript Promise_开发语言_05

第七节JavaScript Promise_构造函数_06

resolve()中可以放置一个参数,用于向下一个then传递一个值,then中的函数也可以返回一个值传递给then。但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,这一点从刚才的计时器的例子中可以看出来。

reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。

注意:

  • resolve和reject的作用域只有起始函数,不包括then以及其他序列。
  • resolve和reject并不能够使起始函数停止运行,别忘了return。

4、Promise 函数

上述使用计时器实现时,代码还是很长的,所以我们可以将核心代码写成一个Promise函数:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Javascript基础学习</title>
	</head>
	<body>
		<h2>JavaScript Promise</h2>
		<button onclick="yibu()">Promise函数</button>
		<p id="one"></p>
		<p id="two"></p>
		<p id="three"></p>
	</body>

	<script>
		// 实现一个Promise函数
		function myPromise(delay, msg, pID) {
			return new Promise(function(resolve, reject) {
				setTimeout(function() {
					console.log(msg);
					document.getElementById(pID).innerHTML = msg;
					resolve();
				}, delay);
			});
		}

		function yibu() {
			myPromise(4000, "4000ms First", "one").then(function() {
				return myPromise(2000, "200ms Second", "two");
			}).then(function() {
				myPromise(2000, "end", "three");
			});
		}
	</script>
</html>

输出结果:

第七节JavaScript Promise_ecmascript_07

这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。

5、常见的问题(FAQ)

Q: then、catch 和 finally 序列能否顺序颠倒?

A: 可以,效果完全一样。但不建议这样做,最好按 then-catch-finally 的顺序编写程序。

Q: 除了 then 块以外,其它两种块能否多次使用?

A: 可以,finally 与 then 一样会按顺序执行,但是 catch 块只会执行第一个,除非 catch 块里有异常。所以最好只安排一个 catch 和 finally 块。

Q: then 块如何中断?

A: then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch 实现中断。

Q: 什么时候适合用 Promise 而不是传统回调函数?

A: 当需要多次顺序执行异步操作的时候,例如,如果想通过异步方法先后检测用户名和密码,需要先异步检测用户名,然后再异步检测密码的情况下就很适合 Promise。

Q: Promise 是一种将异步转换为同步的方法吗?

A: 完全不是。Promise 只不过是一种更良好的编程风格。

Q: 什么时候我们需要再写一个 then 而不是在当前的 then 接着编程?

A: 当你又需要调用一个异步任务的时候。