JavaScript学习记录05

        笔者最近在学习郭超老师著的《JavaScript快速入门与开发实战》,文章内容主要是本人对书本内容学习的记录。内容不是将书本内容完全复现,只记载了笔者认为较为重要或是容易遗忘的内容。05将陆续更新。

目录

10. Peomise异步编程

10.1 概述

10.2 异步编程的实现

10.2.1 为什么JS是单线程

10.2.2 JS如和实现异步编程

10.3 回调函数

10.3.1 回调函数再理解

10.3.2 回调地狱

10.4 Promise

10.4.1 Promise 对象的语法

10.4.2 Promise 对象的属性和方法

10.4.3 Promise 的认识误区

10.4.4 Promise 的三种状态

10.4.5 Promise 的结果

10.4.6 then 方法详解

10.4.7 catch 方法详解

10.4.8 其他方法

10.5 async关键字

10.5.1 异步函数

10.6 await关键字

10.6.1 综合案例

11.模块化

11.1 模块化概述


10. Peomise异步编程

10.1 概述

        异步编程(Asynchronous Programming)是JavaScript语言的一大特色,同时也是学习JavaScript的一大难点。异步是与同步相对的概念,所以先来了解下同步。

        同步指的是JavaScript代码执行时,严格按照代码的编写顺序依次执行,是连续执行的,意味着下一行代码的执行必须等待前一行代码执行完毕之后才会执行。

       同步代码的好处在于编写简单,而且可以直观地观察程序在每一步地运行状态,程序很好控制。

        同步代码地坏处在于如果某一行代码需要花费地时间很长,那么后面地代码就必须排队等待,拖延了整个程序地执行,浏览器会卡顿、假死。

        为了解决这个问题,JavaScript提出了异步编程。所谓地异步简单来说就是异步代码不按照顺序执行,也就意味着程序的状态不易掌控,但是异步的效率更高。

        异步编程的实现有两种方式:一种是多线程,一种是单线程非阻塞。

10.2 异步编程的实现

10.2.1 为什么JS是单线程

        所谓的线程,其实就是程序的一条执行路径,可以设想,比如去工地搬砖,多线程就是多个工人一起去搬砖,而单线程就是一个人搬砖。显然,多线程的效率更高。

        既然多线程效率更高,为什么不采用多线程呢?原因如下:

(1) 浏览器环境的复杂性

  • DOM 操作的原子性需求:浏览器渲染引擎需要保证 DOM 修改的原子性。如果允许多线程同时操作 DOM,会出现不可预测的渲染结果(如一个线程删除节点,另一个线程修改其样式)。
  • 竞态条件(Race Condition):多线程环境下,事件监听、样式计算等操作的执行顺序难以预测,可能导致界面错乱。

(2) 设计初衷的简单性

        JavaScript 诞生时(1995年)的定位是轻量级脚本语言,用于处理简单的表单验证和页面交互。单线程模型避免了线程同步、锁机制等复杂问题,降低了开发门槛。

(3)事件驱动架构的匹配

  • 非阻塞 I/O 的高效性:单线程配合事件循环(Event Loop)和异步回调机制,能在不阻塞主线程的情况下处理高并发 I/O 操作(如网络请求、文件读写)。
  • 执行顺序的可控性:任务队列机制确保代码按明确顺序执行,避免了多线程环境下的执行时序混乱问题。

10.2.2 JS如和实现异步编程

        前面说了,JavaScript是单线程的语言,意味着每个任务要排队处理,如果前面的某个任务耗时很大,那么后面的任务可能要等很久。为了实现异步编程,JavaScript需要通过回调函数和事件监听的方法。需要明确,回调函数和事件监听本质没有什么区别,只是在不同的场景下叫法不同。

10.3 回调函数

        回调函数(callback)是实现异步编程最简单的方式。简单来说,回调函数就是把异步任务单独写在一个函数里面,等到要执行这个异步任务时,再去调用这个函数。回调函数也叫作钩子函数。

        下面来看一个例子:在页面上点击按钮,实现控制台打印,代码如下:

	<script>
		// 第一步
		console.log(11111);
		
		// 第二步
		document.querySelector('button').onclick = function(){
			console.log(222);
		}
		
		// 第三步
		console.log(333);
		
	</script>

        程序执行后,在控制台中直接打印了“111”和“333”,并没有迟迟地等待用户点击按钮地行为操作,而是给暗流绑定好处理程序之后,程序继续往下执行。下面为代码加上一个延时定时器,再来看看效果,代码如下:

	<script>
		// 第一步
		console.log(11111);
		
		// 第二步
		setTimeout(function(){
			console.log(222);
		},1000)
		
		// 第三步
		console.log(333);
		
	</script>

        程序依次打印了“111”,“333”,“222”,并没有等到1s后再去执行“333”的打印,说明JavaScript虽然是单线程的,但是确实可以执行异步任务,即不会等上一个任务结果返回再执行下一个任务。

10.3.1 回调函数再理解

        回调函数也是函数的一种,也是需要调用才能执行,只不过回调函数的执行需要一定的触发时机,而这个触发时机往往是不确定的。以上述代码为例,当程序执行到setTImeout()函数时,发现函数是一个异步函数,此时JS引擎会继续往下执行且完毕之后,待1s时间到了时,程序由掉头回去调用了setTimeout()函数中的参数函数,这个参数函数就叫做回调函数,又好像是一个钩子,所以也叫钩子函数,而回调函数(钩子函数)中封装的就是异步的任务。

setTimeout(callback, delay)

10.3.2 回调地狱

        现在需要开启四个延时定时器,分别是T1,T2,T3,T4,而每个延时定时器所要延迟的时间是不固定的,但是有个要求,这四个延时定时器必须按顺序执行,并在控制台依次输出T1,T2,T3,T4。

        分析:要开启四个延时定时器,就意味着要调用 setTimeout() 四次,需要这四个延时定时器依次执行,并且在控制台按照指定的顺序输出,为了保证顺序,就必须在第一个定时器执行完毕后再去开启第二个定时器,依次类推,开启延时定时器的动作应该在每一个延时定时器的回调函数里面,同时为了实现延时时间不固定,可以采用随机数。示例代码如下:

	<script>
		// 开启四个延时定时器,依次执行,输出T1,T2,T3,T4
		setTimeout(function(){
			console.log('T1');
			setTimeout(function(){
				console.log('T2');
				setTimeout(function(){
					console.log('T3');
					setTimeout(function(){
						console.log('T4');
					},Math.random()*100)
				},Math.random()*100)
			},Math.random()*100)
		},Math.random()*100)
		
	</script>

代码的执行流程入下:

// 初始调用
setTimeout(T1回调, 随机延迟1)
  ↓
// 当随机延迟1到期 ▼
执行 console.log('T1') → 创建 T2 定时器(随机延迟2)
                        ↓
                       // 当随机延迟2到期 ▼
                       执行 console.log('T2') → 创建 T3 定时器(随机延迟3)
                                           ↓
                                          // 当随机延迟3到期 ▼
                                          执行 console.log('T3') → 创建 T4 定时器(随机延迟4)
                                                              ↓
                                                             // 当随机延迟4到期 ▼
                                                             执行 console.log('T4')

        用现实场景类比一下,假如我们去餐厅吃饭,想象一下餐厅点餐的流程:

1.异步操作:服务员提交你的订单给厨房(setTimeout)

2.处理结果:厨师完成菜品后(定时器到期),服务员把菜端上来(执行回调)

3.嵌套操作:你吃完前菜后(处理结果),要求服务员再上菜(发起新的异步操作)

        对比一下,如果不使用嵌套,会发生什么?

	<script>
		// 四个独立定时器(错误示例)
		setTimeout(() => console.log('T1'), Math.random()*100)
		setTimeout(() => console.log('T2'), Math.random()*100)
		setTimeout(() => console.log('T3'), Math.random()*100)
		setTimeout(() => console.log('T4'), Math.random()*100)
	</script>

        此时输出顺序将完全随机(如可能输出 T3 → T1 → T4 → T2),因为所有定时器同时开始计时。

        上面的代码逻辑很好理解, 虽然也实现了需求,但是代码的阅读就很费劲了,回调函数层层嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件,这种现象就是“回调地狱”。那这个问题如何解决,或者说代码如何优化呢?此时Promise解决方案就诞生了。

10.4 Promise

        在早期,JavaScript处理异步操作都是以回调函数(callback)的方式实现的,但是随着ES6的提出,Promise完全改变了JavaScript异步编程的编码方式,使得JavaScript实现异步编程、处理回调地狱问题变得非常简单。

        为什么Promise可以解决回调地狱问题?在讨论这个问题之前,先来回顾下回调函数为什么会造成回调地狱。原因在于执行异步的操作和处理异步的结果这两个步骤写在了一起,下一个异步任务的执行是需要依赖上一个异步任务的结果的,只有上一个异步任务结束了下一个异步任务才能触发,这也就是导致产生层层回调,从而发生回调地狱的根源。

        那么能否将这两个步骤分开呢?也就是执行异步操作处理异步的结果这两个步骤分开,先统一执行异步任务,不关心如何处理结果,然后根据结果是成功还是失败,在将来的某个时刻再去处理成功或失败的业务逻辑,这样的话,该执行异步操作的去执行异步操作,最后统一处理异步操作的结果,因为异步操作执行完毕后,总是会有结果的。Promise就是采用这种思想。

        把异步操作交给Promise来处理,至于这个异步操作的结果什么时候返回,并不需要关心,待有了异步操作的结果,Promise会告知我们,当然,如果又多个异步操作,那么就分别交给每一个Promise来执行,这样就可以将异步的操作用同步的编码风格描述出来,避免了层层回调嵌套,从而解决了回调地狱的问题,这是Promise的根本思想。

        Promise 的本质是一个构造函数,是一个容器,里面存储着某个未来才会结束的事件(通常是一个异步操作)的结果,不论是成功还是失败的结果。

10.4.1 Promise 对象的语法

        Promise 是一个构造函数,在使用时需要通过 new 关键字调用,示例代码如下:

	<script>
		const p = new Promise(function(resolve,reject){
			// 执行一个异步操作,根据异步操作的结果判断
			if(异步结果成功){
				resolve(成功的结果);
			}else{
				reject(失败的结果)
			}
		});
		
		// 成功
		p.then(function(result){
			
		});
		
		// 失败
		p.catch(function(error){
			
		})
	</script>

        上面的代码中,首先创建了一个新的Promise构造函数,Promise接收一个函数作为参数,称为 executor,我们需要处理的异步任务就写在该函数体内,该函数的两个参数是resolve、reject,这两个参数各自分别是一个函数或者说方法。当异步任务执行成功时调用resolve函数返回结果,反之调用reject。Promise 对象的then方法用来接收处理成功时响应的数据,catch方法用来接收处理失败时响应的数据。

        下面用一个用户登录的案例来实际演示一下:

<script>
	// 模拟异步登录验证(实际开发中这里可能是Ajax请求)
	function login(username, password) {
	  return new Promise(function(resolve, reject) {
		// 模拟异步操作(这里用setTimeout代替真实网络请求)
		setTimeout(() => {
		  // 模拟服务器验证逻辑
		  const hasValidUsername = username === "admin";
		  const hasValidPassword = password === "123456";

		  if(hasValidUsername && hasValidPassword) {
			// 模拟返回用户数据
			resolve({
			  id: 1001,
			  username: "admin",
			  token: "x1y2z3"
			});
		  } else {
			// 模拟返回错误信息
			reject(new Error(
			  !hasValidUsername ? "用户名不存在" : "密码错误"
			));
		  }
		}, 1500); // 模拟1.5秒网络延迟
	  });
	}

	// 使用示例
	const loginPromise = login("admin", "123456");

	// 处理异步结果
	loginPromise
	  .then(function(userData) {
		console.log("✅ 登录成功,欢迎", userData.username);
		console.log("完整响应数据:", userData);

		// 这里可以继续返回新的Promise实现链式调用
		return userData.token;
	  })
	  .then(function(token) {
		console.log("🔑 获取到令牌:", token);
	  })
	  .catch(function(error) {
		console.error("❌ 登录失败:", error.message);
		console.log("建议操作:检查输入或联系管理员");
	  })
	  .finally(() => {
		console.log("🔒 登录流程结束");
	  });
</script>

10.4.2 Promise 对象的属性和方法

        既然Promise本质上是一个构造函数,那么就可以通过该构造函数去创建实例对象,并且可以查看该对象身上的属性和方法,示例如下:

<script>
	let p = new Promise(function(resolve,reject){
		
	})
	console.log(p);
</script>

方法:

1.then()
用于处理 Promise 成功(fulfilled)的回调,也可接收失败(rejected)的回调(非必需)。返回一个新 Promise,支持链式调用。

promise.then((value) => { /* 处理成功 */ }, (reason) => { /* 处理失败 */ });

2.catch()
专门处理 Promise 拒绝(rejected)的情况,等价于 then(undefined, rejectionCallback)

promise.catch((reason) => { /* 处理失败 */ });

3.finally()
无论 Promise 是成功还是失败,都会执行的回调,常用于资源清理。返回一个新 Promise,状态与原 Promise 一致。

promise.finally(() => { /* 通用清理操作 */ });

4.constructor
Promise 的构造函数,用于创建 Promise 实例,接收一个执行器函数(executor)。

const promise = new Promise((resolve, reject) => { /* 执行器逻辑 */ });

内部属性(引擎内部管理,不可直接访问):

  • [[PromiseState]]:表示 Promise 的状态,有三种值:
    • pending(初始状态)
    • fulfilled(已成功)
    • rejected(已拒绝)。
  • [[PromiseResult]]:保存最终结果或原因。pending 时为 undefined,成功时为 resolved 值,失败时为 rejection 原因。
  • [[Prototype]]:指向 Promise.prototype,包含 thencatchfinally 等原型方法。

10.4.3 Promise 的认识误区

        先来看一段代码,想想程序执行的结果是什么。

<script>
	console.log('111');
	new Promise(function(resolve,reject){
		console.log('222');
		// 模拟异步操作
		setTimeout(function(){
			console.log('333');
			console.log('执行了');
		},2000)
	});
	console.log('444');
</script>

执行结果如下:

        分析结果可知,创建Promise实例对象后,executor函数会立刻执行。即executor函数在Promise构造函数执行时同步执行;Promise本身不是异步的,是同步的,只不过Promise这个容器中所保存要执行的操作往往是异步的。

10.4.4 Promise 的三种状态

        Promise 是一个由状态的对象,存在三种状态,分别是是pending(初始状态、待定状态)、fulfilled/resolved(已兑现、成功)和rejected(已拒绝、失败),而且状态的转换过程有且仅有两种,pending变为fulfilled或者pending变为rejected,状态的改变是一次性的,不会存在fulfilled变为rejected状态。可以通过resolve函数或者reject函数去改变Promise的状态。

        案例一:将pending状态转换为fulfilled。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		// 调用该函数(成功函数),可将pending转换为fulfilled
		resolve();
	})
	console.log(p);
</script>

        案例二:将pending转换为rejected。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		// 调用该函数(失败函数),可将pending转换为rejected
		reject();
	})
	console.log(p);
</script>

10.4.5 Promise 的结果

        前面介绍了 PromiseState 的状态属性,接下来介绍 PromiseResult 属性,该属性表示异步操作执行后的结果。可以发现,之前的 PromiseResult 属性值都是undefined。那么如何改变这个值?

        对于resolve这个参数来说,它是一个函数,可以通过调用该函数实现对状态的改变,既然是函数,就可以在调用时传递参数,reject函数同理。

        测试一:调用resolve函数传递参数。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		resolve('异步操作成功的结果');
	})
	console.log(p);
</script>

        测试二:调用reject函数传递参数。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		reject('异步操作失败的结果');
	})
	console.log(p);
</script>

10.4.6 then 方法详解

        then 是Promise原型对象身上的方法,那么通过Promise构造函数创建的实例对象也就有了then方法,then方法可以接收两个参数,这两个参数各自是一个函数,分别为成功的回调函数和失败的回调函数,成功的回调函数和失败的回调函数同时还可以接收参数,分别是res 和 error,对于then 方法,第二个失败的回调函数参数可以省略。then 方法语法如下:

<script>
	let p = new Promise(function(resolve,reject){
		
	});
	p.then(function(res){
		
	},function(reeor){
		
	});
</script>

        下面来介绍then 方法的两个参数:

        测试一:then 方法的第一个参数函数的调用时机以及res的值是什么。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		resolve('获取异步操作成功的结果')
	});
	p.then(function(res){
		console.log('success:',res);
	},function(error){
		console.log('fail:',error);
	});
</script>

        第一,程序调用了resolve 函数,当前Promise实例对象状态改为fulfilled(成功);第二当Promise 实例对象状态变成fulfilled时,then 方法中的第一个参数函数被调用;第三,调用resolve函数传递的参数会作为then方法第一个参数函数的参数进行接收。

        简单来说,Promise实例对象成功执行后(即resolve成功执行),接着执行then方法中的第一个函数,并且参数是resolve函数中的内容。

        实际上调用resolve函数,就是在调用then方法的第一个参数函数,调用resolve函数传递的参数将作为then方法的第一个参数函数的参数res。

        测试二:then方法的第二个参数函数的调用时机及error的值是什么。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		reject('异步操作失败的结果')
	});
	p.then(function(res){
		console.log('success:',res);
	},function(error){
		console.log('fail:',error);
	});
</script>

        第一,调用了reject函数,当前Promise实例对象状态改为rejected(失败);第二,当Promise实例对象的状态变为rejected时,then方法中的第二个参数函数被调用;第三,调用reject函数传递的参数会作为then方法第二个参数函数的参数进行接收。

        总而言之,这两个参数的使用方法是类似的,只不过一个代表异步操作成功后执行的内容,另一个代表异步操作失败后执行的内容。

        介绍了then方法的参数,下面来介绍下then方法的返回值。不妨用一个变量r接收,代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		resolve('异步操作成功的结果')
	});
	let r = p.then(function(res){
		console.log('success:',res);
	},function(error){
		console.log('fail:',error);
	});
	console.log(r);
</script>

        通过控制台查看结果,then方法得到一个结果,这个结果是新的Promise对象,状态还是funlfilled,这里就有一个疑问,为什么返回的新Promise对象的状态是成功的状态呢?那么就有必要讨论返回新的Promise对象的状态是受什么影响的。下面看几个案例。

        测试一:代码如下

<script>
	let p = new Promise(function(resolve,reject){
		resolve('异步操作成功的结果')
	});
	let r = p.then(function(res){
		console.log('success:',res);
		
		// 直接打印一个没有定义的 a 变量,此时是报错的
		console.log(a);
		
	},function(error){
		console.log('fail:',error);
	});
	console.log(r);
</script>

        调用的是resolve函数,该函数会改变p对象的状态,改为fulfilled,所以then方法的第一个参数函数就会被调用,但是在执行该函数的过程中程序出现了错误,通过控制台查看结果,新的Promise对象的状态为rejected,结果就是出错的信息。

        

        测试二:代码如下

<script>
	let p = new Promise(function(resolve,reject){
		reject('异步操作失败的结果')
	});
	let r = p.then(function(res){
		console.log('success:',res);
	},function(error){
		console.log('fail:',error);
	});
	console.log(r);
</script>

        程序调用的是reject函数,该函数会改变p对象的状态,改为rejected,所以then方法的第二个参数函数就会被调用,并且在执行该函数的过程中程序没有任何异常,会有一个默认的undefined返回,但是通过控制台查看结果,新返回的Promise对象(对象r)的状态是fulfilled,结果就是return后的值。

        结论:在then方法的两个参数函数中,函数的返回值如果是非Promise类型的,那么then方法的返回值就是fulfilled状态;如果回调函数出现了错误,则新Promise的状态就是rejected。

        接下来在深入思考,then方法返回的是新Promise对象,而新对象的状态取决于回调函数的返回,同时还可以继续调用r对象身上的then方法,下面来观察一下返回的新Promise对象的then方法的两个参数函数。代码如下:

<script>
	let p = new Promise(function(resolve,reject){
		resolve('异步执行成功')
	});
	let r = p.then(function(res){
		console.log('success:',res);
		return 'helloworld';
	},function(error){
		console.log('fail:',error);
	});
	
	r.then(function(res){
		console.log(res);
	})
</script>

        p实例对象中then方法的第一个参数正常返回了一个字符串“helloworld”,则这个返回结果就作为了then方法返回新Promise对象的异步成功的结果,也正是如此,所以打印的结果res就是::helloworld。

        简单来说,return了什么结果,那么返回新Promise实例对象的then方法中的参数函数中的参数的值就是什么。

        再想想,then方法的两个参数函数除了返回普通的值之外,能否返回一个 Promise 对象呢?代码如下:

<script>
	let p1 = new Promise(function(resolve,reject){
		resolve('p1---成功了');
	})
	let p2 = new Promise(function(resolve,reject){
		resolve('p2---成功了');
	})
	
	p1.then(function(res){
		console.log(res);
		// 返回 Promise 对象
		return p2;
	}).then(function(res){
		console.log(res);
	})
</script>

        答案明显是能。首先创建 p1 和 p2,它们的构造函数中直接调用了resolve函数,所以这两个 Promise 会立即进入fulfilled 状态,并分别保存结果值“p1---成功了”“p2---成功了”。代码执行到p1.then()这部分,当p1完成时,它的回调函数会被推入到微任务队列(JS时间循环的机制),回调执行时会输出“p1---成功了”并返回p2,同样p2处于完成状态,所以直接执行内部函数。

总结:

  1. 同步代码执行:创建 p1p2,两者立即完成。
  2. 第一个 .then() 的微任务:输出 p1---成功了,返回 p2
  3. 第二个 .then() 的微任务p2 已解决,输出 p2---成功了

        最后我们回到最开始的一个任务,依次输出T1,T2,T3,T4,这里采用Promise的方法,代码如下:

<script>
	let p1 = new Promise((resolve,reject) => {
		setTimeout(function(){
			resolve('T1');
		},Math.random()*100);
	});
	let p2 = new Promise((resolve,reject) => {
		setTimeout(function(){
			resolve('T2');
		},Math.random()*100);
	});
	let p3 = new Promise((resolve,reject) => {
		setTimeout(function(){
			resolve('T3');
		},Math.random()*100);
	});
	let p4 = new Promise((resolve,reject) => {
		setTimeout(function(){
			resolve('T4');
		},Math.random()*100);
	});
	
	// 每个then方法中参数函数的返回值是Promise 对象
	p1.then(function(res){
		console.log(res);
		return p2;
	}).then(function(res){
		console.log(res);
		return p3;
	}).then(function(res){
		console.log(res);
		return p4;
	}).then(function(res){
		console.log(res);
	})
</script>

        从编码风格上看,虽然代码量貌似增多了,但是实际上通过Promise方式实现的好处就是用编写同步代码的编码风格实现了异步编程,避免回调函数层层嵌套。这样便于阅读。

        同时对于上述代码,会发现,启动定时器的代码是重复性的,可以考虑函数封装,代码如下:

<script>
	// 封装启动定时器异步代码
	function start(timerName){
		return new Promise((resolve,reject)=>{
			setTimeout(function(){
				resolve(timerName);
			},Math.random()*100);
		})
	}
	
	start('T1').then(function(res){
		console.log(res);
		return start('T2');
	}).then(function(res){
		console.log(res);
		return start('T3');
	}).then(function(res){
		console.log(res);
		return start('T4');
	}).then(function(res){
		console.log(res);
	})
</script>

10.4.7 catch 方法详解

        then方法接收两个参数函数,即then(fn1,fn2),而fn2函数的执行时机是当Promise实例对象的状态改为rejected触发失败回调时,该函数就会执行。

        在Promise原型对象身上,有一个catch方法,该方法的作用也是当Promise实例对象的状态改为rejected时,该catch方法就会触发。

        实际上,catch方法的本质就是then方法的语法糖形式,即then(null,fn2)。下面对catch方法做一个总结。以下形式会触发catch方法的执行,分别是:

  • 当Promise的状态改为rejected时被触发;
  • 当Promise执行过程中出现代码错误时被触发;
  • 当Promise执行过程中出现手动抛出异常时触发;
  • then方法指定的回调函数,如果运行中抛出错误,会被catch方法捕获,catch方法被触发。

        对于then方法更加标准的写法是:不仅要写成功的回调,还需要写失败的回调,这样就可以进行错误的处理了。此时有个麻烦的地方是,如果then方法存在链式调用的话,每次使用then方法都需要编写失败回调函数,这样着实有些麻烦,代码形式如下:

<script>
	let p1 = new Promise((resolve,reject)=>{
		
	});
	let p2 = new Promise((resolve,reject)=>{
		
	});
	
	p1.then(function(res){
		return p2;
	},function(error){
		
	}).then(function(res){
		
	},function(error){
		
	})
</script>

        catch方法有个异常穿透的特点,简单来说,当程序运行到最后,没被处理的所有异常错误都会进入这个catch方法的回调函数中。利用这个特性,可以统一实现对错误的处理。代码如下:

<script>
	let p1 = new Promise((resolve,reject)=>{
		
	});
	let p2 = new Promise((resolve,reject)=>{
		
	});
	
	p1.then(function(res){
		return p2;
	}).then(function(res){
		
	}).catch(function(error){
		
	})
</script>

10.4.8 其他方法

        在Promise身上还有其他的静态方法,例如all() any() trace() 方法,下面讲讲这三种方法。

(1)Promise.all() —— "全成功才成功"

特点:

  • 全部成功时返回结果数组,任意一个失败则立即抛出错误
  • 保持顺序:结果数组顺序与输入 Promise 顺序一致
  • 原子性:一旦有失败,整个操作立即终止
<script>
	const p1 = Promise.resolve(1);
	const p2 = Promise.resolve(2);
	const p3 = Promise.reject("Error");

	Promise.all([p1, p2])
	  .then(console.log) // [1, 2](全部成功)
	  .catch(console.error);

	Promise.all([p1, p3, p2])
	  .then(console.log)
	  .catch(console.error); // "Error"(遇到第一个失败)
</script>

典型场景

  • 需要同时获取多个接口数据后才能渲染页面
  • 并行执行多个任务,且所有任务必须成功

(2)Promise.race() —— "竞速赛跑"

特点

  • 第一个完成(无论成功/失败)的 Promise 决定最终结果
  • 其他未完成的 Promise 仍会继续执行(但结果被忽略)
<script>
	const fast = new Promise((resolve) => 
	  setTimeout(() => resolve("Fast"), 100)
	);
	const slow = new Promise((resolve) => 
	  setTimeout(() => resolve("Slow"), 500)
	);
	const error = new Promise((_, reject) => 
	  setTimeout(() => reject("Error"), 200)
	);

	// 成功竞速
	Promise.race([fast, slow])
	  .then(console.log); // "Fast"(第一个完成)

	// 失败竞速
	Promise.race([error, slow])
	  .catch(console.error); // "Error"(第一个完成)
</script>

典型场景

  • 网络请求超时控制(请求 vs 超时定时器)
  • 多服务器探测,优先响应最快的节点

(3)Promise.any() —— "第一个成功者胜出"

特点

  • 等待 第一个成功的 Promise
  • 只有 全部失败 时才抛出 AggregateError(包含所有错误)
<script>
	const p1 = Promise.reject("Error1");
	const p2 = new Promise((resolve) => 
	  setTimeout(resolve, 500, "Slow success")
	);
	const p3 = new Promise((resolve) => 
	  setTimeout(resolve, 100, "Fast success")
	);

	// 有成功的情况
	Promise.any([p1, p2, p3])
	  .then(console.log); // "Fast success"(第一个成功)

	// 全部失败的情况
	Promise.any([p1, Promise.reject("Error2")])
	  .catch(e => console.error(e.errors)); // ["Error1", "Error2"]
</script>

典型场景

  • 多 CDN 源择优(使用第一个可用的资源)
  • 冗余请求,确保至少一个成功

10.5 async关键字

        async关键字的作用是修饰一个普通函数,被async修饰的普通函数会作为一个异步函数,是ES7提供的新特性,是一个语法糖,异步函数基于Promise构造函数。

10.5.1 异步函数

<script>
	async function fn(){
		return 'hello';
	}
	
	console.log(fn());
</script>

        上述代码中fn函数就是一个异步函数,调用fn函数返回Promise对象,Promise对象的状态取决于fn函数内部能否正常return。在异步函数内部使用return返回,结果会被包裹在promise对象中,return关键字代替了resolve方法。

        return可以返回普通值,也可以返回Promise对象。代码如下:

	<script>
		async function fn(){
			return new Promise(function(resolve,reject){
				resolve('获取异步操作的结果');
			})
		}

		console.log(fn());
	</script>

        如果返回的是一个Promise对象,那么最终fn函数的返回值Promise的状态取决于是执行力resolve函数还是reject函数,结果是调用resolve函数的参数。

10.6 await关键字

(1)基本概念

1. await 的作用

  • 暂停执行:在 async 函数中,遇到 await 时会暂停当前函数的执行,等待后面的 Promise 完成

  • 获取结果:若 Promise 成功(fulfilled),await 会返回 Promise 的结果值;若 Promise 失败(rejected),则抛出错误。

2. async 函数是前提

  • await 必须在标记为 async 的函数内部使用:

async function fetchData() {
  const result = await somePromise; // 正确
}

(2)核心行为

1. 等待 Promise 完成

  • 如果 await 后是一个 Promise,函数会暂停,直到该 Promise 变为 fulfilledrejected

async function example() {
  const data = await fetch('https://api.example.com/data'); // 等待请求完成
  console.log(data); // 请求完成后执行
}

2. 处理非 Promise 值

  • 如果 await 后不是 Promise,JavaScript 会将其自动包装成已解决的 Promise

async function demo() {
  const num = await 42; // 等价于 await Promise.resolve(42)
  console.log(num); // 42
}

(3)常见用法

1. 顺序执行异步操作

async function sequentialTasks() {
  const result1 = await task1(); // 等待 task1 完成
  const result2 = await task2(); // 再执行 task2
  return result2;
}

2. 并行执行优化

  • 使用 Promise.all 并行处理多个异步操作:

async function parallelTasks() {
  const [res1, res2] = await Promise.all([task1(), task2()]);
  console.log(res1, res2); // 同时执行,等待全部完成
}

10.6.1 综合案例

        要求启动四个延时定时器,每个定时器的延迟时间不固定, 要求能够依次执行四个定时器,并且依次输出T1,T2,T3,T4。代码如下:

	<script>
		// 封装启动器函数
		async function start(timerName){
			return new Promise((resolve,reject)=>{
				setTimeout(()=>{
					resolve(timerName); // 该参数作为Promise的结果值
				},Math.random()*100);
			})
		}
		
		async function run() {
		  let r1 = await start('T1'); // 等待 T1 完成
		  let r2 = await start('T2'); // 等待 T2 完成
		  let r3 = await start('T3'); // 等待 T3 完成
		  let r4 = await start('T4'); // 等待 T4 完成
		  console.log(r1, r2, r3, r4); // 输出结果
		}
		run();
	</script>
  • 每个 await阻塞后续代码,必须等待当前 Promise 完成后才会执行下一个

等效代码:

start('T1').then(r1 => {
  start('T2').then(r2 => {
    start('T3').then(r3 => {
      start('T4').then(r4 => {
        console.log(r1, r2, r3, r4);
      });
    });
  });
});

11.模块化

        由于历史原因,JavaScript在使用时存在两大问题,一是命名冲突,二是文件依赖。为解决这两个问题,ES6引入了模块化的概念。

        什么是命名冲突?假设有两个JS脚本文件,文件中可能存在相同的变量或是函数定义,此时就存在命名冲突。

        什么是文件依赖?简单来说,就是一个js文件需要引用另一个js文件,在一个大项目中,如果各种js脚本都存在这样的依赖关系,那么文件管理就很麻烦,为了使用一个js文件,还不得不了解这个js文件需要引入那些js文件,这样效率很低。

11.1 模块化概述

        一个项目是由各种模块组成的,通过不同模块的通力协作完成了整个系统的运行,其中一个模块的损坏也不会影响其他模块的正常运行,力求把影响降到最低。

        ES6的模块化开发就规定了每个js文件都是一个模块,模块代码以严格模式执行,模块内部定义的变量和函数不会添加到全局作用域中。同时模块化开发没有全局作用域,只有局部作用域。

        在ES6之前,浏览器端需要使用require.js来实现模块化开发,在Node.js中使用CommonJS规范来模块化编程,JavaScript没有统一的标准规范,所以ES6推出了标准的模块化方案。

        模块化开发主要由两个命令构成的:export和import。

  • export 该命令的作用是对外暴露模块中定义的成员,只有这样外部才能访问到模块中定义的成员(包括变量、常量、函数、类);
  • import 该命令的作用是导入其他模块,这样就可以使用其他模块中export对外暴露的成员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值