js异步等待完成后再进行下一步操作_Js Promise 与 异步操作

本文详细介绍了JavaScript中的异步操作,包括为何需要异步、异步加载图片、回调地狱问题以及Promise的用法。Promise用于解决异步代码的回调问题,提供了一种更好的错误捕获和执行顺序控制方式。接着,文章讲解了async/await作为Promise的语法糖,简化了异步代码的编写,使得异步操作更加直观。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

~为什么需要异步操作?

比起同一时间只能干一件事,我们总是追求更高的效率,一边吃饭一遍写代码还能看直播。

比如说当我们请求一个接口,后台响应的时间非常长,如果没有异步操作,程序只能傻等,进行不了下一步操作。

Js只有一个主线程执行,如果说我们能把复杂的需要时间处理的任务,交给一个模块去处理,不占用主线程,等它处理结束后,再得到响应就好了

举个例子:我们要做吃饭、洗衣服、和看直播这三件事

这三件事就会放入我们的任务队列中,如果以同步的思想来看的话,我们只能先吃饭再洗衣服,后看直播。这很傻,意味着我们做一件事情时不能做其他的。

我们可以看直播,把衣服交给洗衣机,做饭交给电饭煲。等他们做好后滴的一声提示我,去晾衣服,和吃饭。

需要注意的是,衣服可以交给洗衣机,就像等待服务器响应一样我可以做其他的东西。但是最后晾衣服完成洗衣服的动作,任然是需要主线程处理的。

~当主线程中的同步代码代码执行时间十分长

		function a(){
			console.log(1)
		}
		function b(){
			console.log(2)
		}
		a()
		b()

ea374028d5b8c95df1883747a585ffa3.png
这是十分理想的状态,主线程依次执行a和b,以为计算量不大执行时间都不长没有影响到b。
		function a(){
			for(let i=0;i<1000000000;i++){
				if(i==999900000){
					console.log(i)
				}
			}
		}
		function b(){
			console.log(2)
		}
		a()
		b()

76d1fdaeb01c43e6983b07453e490b3c.png
b在队列中等待,直到a执行完成,当a执行时间很长,这种等待有时是无法忍受的,这时我们需要异步操作
		function a(){
			setTimeout(()=>{
				for(let i=0;i<1000000000;i++){
					if(i==999900000){
						console.log(i)
					}
				}
			},0)
		}
		function b(){
			console.log(2)
		}
		a()
		b()

52caaf7d93ba5c7532b8a5435b534476.png
将a转为异步时,只有b在主线程,他会立刻执行

~eg:异步加载图片

		function loadImg(url,resolve,reject){
			let img = new Image()
			console.dir(img)
			img.src=url
			img.onload=()=>{
				resolve(img)
			}
			img.onerror=reject
		}

		loadImg('https://i1.hdslb.com/bfs/face/a3d75e10c363fccfe994b708ddb19caa2e6e1c5e.jpg@52w_52h.webp',
			(img)=>{
				document.body.appendChild(img)
				console.log('resolve')
			},
			()=>{
				console.log('reject')
			},
		)
               console.log(123)

处理图片的时间显然很长,Js将其转为异步执行,这其中可能执行成功可能失败,我们用回调接收响应。就上文所说,做饭交给电饭煲,做好了通知我去吃,做糊了我去重做,期间我们可以去做的别的事情,就像下面执行的同步代码输出123。

~当两个异步操作同时触发,如何控制先后

		function js(url,call){
			let script=document.createElement('script')
			script.src=url
			document.body.appendChild(script)
			script.onload=call
		}

			js('one.js',()=>{
				one()
			})
			js('two.js',()=>{
				two()
			})

77b082adf6e9658dd336a3756d12c211.png

08f2c7000d665c006dc0d49c7f9a563e.png
js异步加载了两个js文件,但因为他们都是异步的,无法控制先后

如果想让two在one.js加载后再加载,最简单的方法就是,在one的回调中,加载two

		js('one.js',()=>{
			one()
			js('two.js',()=>{
				two()
			})
		})

但是这样有一个问题,当业务逻辑非常复杂时,不停的在嵌套,出现回调地狱有两个问题,第一是代码非常不美观冗余,第二当在多层嵌套中,有一层出现了问题,我们无法处理捕获错误,只能在其中判断,造成代码更加冗余。

4f4a528fc9d1daca4c2fc1623496ae9e.png

Promise

~用来做什么?

从上面的内容我们可以了解到,书写代码中回遇到代码嵌套造成的回调地狱,以及代码执行中的错误捕获,还有异步代码执行时的先后顺序,Promise 这个Es6中新加入的内容,就是用来解决这一系列的问题。

~Promise 的基本用法

~~创建promise对象

new Promise((resolve,rejeck)=>{

})  //构造函数创建一个新的 Promise对象

~~promise状态

promise有两个参数,resolve和reject,分别对应成功响应与失败响应,当没有抛出失败或成功是,这个promise对象就在pending等待状态

console.log(new Promise((res,rej)=>{}))

c2819547c9afdb5b4df0e9ef52e411c5.png
等待状态
console.log(new Promise((res,rej)=>{res(1)}))

98ab45e0f9edb6a477c87dd4b90adcb9.png
成功状态
console.log(new Promise((res,rej)=>{rej(1)}))

1a8c9afc7d28122e71752bae697c48f1.png
失败状态

~~成功与错误捕获

当Promise对象状态发生改变,就由then或catch捕获

new Promise((res,rej)=>{
   res('成功')
})
.then((res)=>{
   console.log(res)  //'成功' then第一个参数捕获成功
})

new Promise((res,rej)=>{
   rej('失败')
})
.then((res)=>{
   console.log(res)  
})
.catch((error)=>{
   console.log(error)  //'失败' 利用catch捕获失败
})

new Promise((res,rej)=>{
   rej('失败')
})
.then((res)=>{
   console.log(res)  
},error=>{
   console.log(error)   //'失败' 利用then第二个参数捕获失败
})

~~then()执行顺序

上面提到then中的代码时等待promise对象做出响应后才开始执行,表明它不是同步代码,事实也确实如此,then放在微任务队列等待执行,等待本轮主线程执行完毕后执行。具体可以看之前的 工作笔记 JS任务机制

~~then的链式调用

then中的代码都是微任务,在微任务队列中他们也按照顺序执行,可以理解为某种的同步,

two		new Promise((res)=>{
			res(1)
		})
		.then((res)=>{
			console.log('one')
		})
		.then(()=>{
			console.log('two')
		})

//先输出one后输出two

		new Promise((res)=>{
			res(1)
		})
		.then((res)=>{
			for(let i=0;i<9999;i++){
				for(let x=0;i<9999;x++){
					console.log(1)
				}
			}
		})
		.then(()=>{
			console.log('two')
		})

//先输出1后输出two

		new Promise((res)=>{
			res(1)
		})
		.then((res)=>{
			setTimeout(()=>{
				console.log(1)
			},0)
		})
		.then(()=>{
			console.log('two')
		})
//先输出two后输出1

~~Promise链式调用

		let promise=new Promise((res,rej)=>{
			res('成功')
		})
		new Promise((res)=>{
			res(promise)
		})
		.then((res)=>{
			console.log(res) //'成功'
		})

Promise改变状态是其他Promise时,执行then时需要等待父级的响应

		let promise=new Promise((res,rej)=>{
			setTimeout(()=>{
				console.log(1)
			},0)
			res('成功')
		})
		new Promise((res)=>{
			console.log(2)
			res(promise)
		})
		.then((res)=>{
			console.log(res)
		})

// 2 成功 1
2是同步代码
成功是微任务
1是宏任务
输出的then的成功,需要父级promise的响应

~~Promise状态不可逆

在promise中执行到一个状态后,then的代码已经放在微任务队列中了,这个promise对象的状态永久的改变了并且无法改变。

32b9546dffc802d351d09e579cf73db2.png
不会走失败

~~每个then都是一个Promise

52a3e792db6c2947e6d527aaaae5ad48.png
打印出来的p2也是一个promise

c795cd130ebb9b90562b6e2cfcc9a3a7.png
状态的改变,注意任务执行顺序,这里就不强调了

6947df0d5ceff932df2551eab026cd56.png
为新的then传递参数

49748945bc0776c1161066c8ee3ecc66.png

~~Prmise自己的then和链式的then哪个先触发?

一句话里面的then就是外面then的处理,并会被外面捕获

70c39c5f57e46d444f03a903ee2ec9bf.png
我们上面说过每一个then返回的都是一个新的Promise,哪么p2有两个then,一个是自己retrun出去的then,一个是外面链式调用的then,哪么会先执行哪个呢?

b3761dfdcd540f2344e1688f9f83a201.png
自己的then先触发,return出去的数据外面的then捕获

~~Promise任务队列

所谓任务队列就是一个个排序,依次执行。通过上面的学习,我们可以通过then来进行队列执行。但需要注意的是,每个then都需要返回一个Promise而且它的状态需要改变

6f72ae0ad390f3df23f33c3cd65c8b69.png

4a06239707771c467be9d2984e989dc6.png
间隔一秒后输出

通过上面的学习,我们可以改写为这样

61b2dd37680be0e68c427ce98d02b13b.png
注意promise=promise.then()是赋值语句,等待右侧的执行结果,可以想象为5个then链式调用在一起

如果我们请求两个彼此依赖接口实现队列,我们可以这样写

		function p(arr){
			let promise=Promise.resolve()
			arr.map(item=>{
				promise=promise.then(()=>{
					return item()
				})
			})
		}
		function p1(){
			return new Promise(res=>{
				setTimeout(()=>{
					res()
					console.log(1)
				},1000)
			})
		}
		function p2(){
			return new Promise(res=>{
				setTimeout(()=>{
					res()
					console.log(2)
				},1000)
			})
		}
		p([p1,p2])

35d359e1f6078a92234cabd96b93cb3e.png
循环的时候then必须返回一个Promise并且状态改变,这一点需要记住

换成reduce更简洁一些

		function p(arr){
			arr.reduce((promise,item)=>{
				return promise.then(res=>{
					return item()
				})
			},Promise.resolve())
		}
		function p1(){
			return new Promise(res=>{
				setTimeout(()=>{
					console.log(1)
					res()
				},1000)
			})
		}
		function p2(){
			return new Promise(res=>{
				setTimeout(()=>{
					console.log(2)
					res()
				},1000)
			})
		}
		p([p1,p2])

模拟工作环境

ae01806823b915d8f9453d043f232f6f.png

13a6c81df6b13e1f45b2ffed22c61802.png

14db32a1a28f76e8a721273c9bddb4a4.png
队列请求,这是不用then链式调用和async的解决办法

~~catch() 与 then()捕获错误的区别

一句话处理自身的Promise的用then第二个参数,链式调用底部用catch兜底

~~finally()

不论Promise成功还是失败都会执行finally,和then相同在Promise后面调用.

应用场景如请求一个接口,不论成功失败最后都要取消掉loding动画我们就可以放在finally里

~~Promise封装异步加载图片

上文我们用回调函数创建了的加载图片,我们学习了Promise可以改造一下

		function image(url){
			return new Promise((res,rej)=>{
				let img=new Image()
				img.src=url
				img.onload=()=>{
					res(img)
				}
				img.onerror=rej
				document.body.appendChild(img)
			})
		}
		image('https://i2.hdslb.com/bfs/face/7e998fc9ba59e2e79a39bfdb620b5cdbb7ea04e3.png@86w_86h.webp').then(
			(res)=>{
				let img=res
				img.style.border='10px solid red'
			}
		)

~~Promise封装定时器

		function timeOut(time=1000){
			return new Promise((res,rej)=>setTimeout(()=>{res()},time))
		}
		timeOut(2000).then(
			()=>{
				console.log(123)
				return timeOut(1000)
			}
		)
		.then(()=>{
			console.log(456)
		})

~~使用Promise异步改变样式

	        .div{
			width: 200px;
			height: 200px;
			background: red;
			position: absolute;
		}	


                let div=document.querySelector('.div')
		function interval(time=10,call){
			return new Promise((res)=>{
				let id=setInterval(()=>{
					call(id,res)
				},time)
			})
		}
		interval(50,(id,res)=>{
			let left=parseInt(window.getComputedStyle(div).left)
			div.style.left=left + 10 + 'px'
			if(left>=300){
				clearInterval(id)
				res(div)
			}
		})
		.then((div)=>{
			return interval(50,(id,res)=>{
				let width=parseInt(window.getComputedStyle(div).width)
				div.style.width=width - 10 + 'px'
				if(width<30){
					clearInterval(id)
					res(div)
				}
			})
		})
		.then((res)=>{
			div.style.background='#333'
		})

尝试一下不使用Promise而是使用回调函数多层嵌套,回变成什么样子

~~all()

等待多个异步执行,后返回结果。最常用的场景app下来刷新,可能要重新请求多个接口,都这些异步接口都请求完毕后,结束lodding动画

		let p1=new Promise((res,rej)=>{
			setTimeout(()=>{
				res(1)
			},1000)
		})
		let p2=new Promise((res)=>{
			setTimeout(()=>{
				res(2)
			},2000)
		})
		let all=Promise.all([p1,p2])
		all.then(
			(res)=>{
				console.log(res,11)
			}
		)

80d4f6dc92282165eff25def73af486b.png
在等待了两秒中后返回,res是promise数组的成功回调

all捕获错误

f292cbc5ecc36a4e3e18d8995eb13d85.png
		let p1=new Promise((res,rej)=>{
			setTimeout(()=>{
				rej(1) //err
			},1000)
		})
		let p2=new Promise((res)=>{
			setTimeout(()=>{
				res(2)
			},2000)
		})
		let all=Promise.all([p1,p2])
		all.then(
			(res)=>{
				console.log(res,11)
			}
			,
			(err)=>{
				console.log(err,22)
			}
		)

f292cbc5ecc36a4e3e18d8995eb13d85.png
我们依然可以捕获错误

~~allSettled()

与all的区别是,all捕获到一个错误,就会被捕捉,而且成功的也不会被resolve捕捉,就像是一个Promise一样状态被永远不可逆改变为失败状态一样。而allSettled则是不管是否有错误,都会走resolve不会被catch捕捉,在resolve中显示成功失败的数组。

		let p1=new Promise((res,rej)=>{
			setTimeout(()=>{
				rej(1) //err
			},1000)
		})
		let p2=new Promise((res)=>{
			setTimeout(()=>{
				res(2)
			},2000)
		})
		let all=Promise.allSettled([p1,p2])
		all.then(
			(res)=>{
				console.log(res,11)
			}
			,
			(err)=>{
				console.log(err,22)
			}
		)

6ee355fc925dc6b54906f7a7067949b5.png
返回promise的状态和返回值

~~race()

race和他的翻译一样赛马,放入多个Promsie,最先执行的完成的返回,成功res失败被catch捕捉

		let p1=new Promise((res,rej)=>{
			setTimeout(()=>{
				rej(1) //err
			},1000)
		})
		let p2=new Promise((res)=>{
			setTimeout(()=>{
				res(2)
			},2000)
		})
		let all=Promise.race([p1,p2])
		all.then(
			(res)=>{
				console.log(res,11)
			}
			,
			(err)=>{
				console.log(err,22) //1 22
			}
		)

async/await

async/await是Es2017加入的是Prmise的语法糖

~~async

在函数前使用,将函数返回为一个Prmise

		async function p1(){
			return 1
		}
		console.log(p1())
		p1()
		.then((res)=>{
			console.log(res) 
		})

76579c4cfb6effade9db21712826f865.png
我们可以看到返回出来一个Promise
		let p1 =new Promise((res)=>{
			res(1)
		}) 
		console.log(p1)

3be1ea1640096b8dca86a032776edd28.png
从上面我可以看到async就是Promise的语法糖

~~await

await等待Promise返回结果

简单的来说 await就是用替代then链式调用的。是得可以更加优雅的书写

如果有多个异步操作需要排列执行,await配合async就非常适合

		async function p(){
			await time()
			await time(3000)
			await time(2000)
		}
		function time(Time=4000){
			return new Promise(res=>{
				setTimeout(()=>{
					res(Time)
					console.log(Time)
				},Time)
			})
		}	
		p()

15c288bdc64f3fceeb9a4169fba8e4f3.png
4s 2s 3s

~~async/awite的错误机制

当多个awite在执行是,有一个执行错误,后续的awite将不再执行

		async function p(){
			let p1=await time()
			console.log(p1)
			let p2=await time(2000)
			console.log(p2)
			let p3=await time(3000)
			console.log(p3)
		}
		function time(Time=1000){
			return new Promise((res,rej)=>{
				setTimeout(()=>{
					if(Time==2000){
						rej('err')
					}
					res(Time)
				},Time)
			})
		}	
		p()

fbf3c02b56f66d8a2862a30cee2af37c.png

这是很常见的情况,请求三个接口,每个都彼此依赖,一个出现错误,下面的都执行不下去。通过上文我们可以很好的用then和catch的捕获机制处理。而用async/awite时我们依然可以捕获

94d8ddabca7729b6a331e0e032b3b7e0.png

ca25feb3894acc94268c785c09a734be.png

297ad278f185a24de217a8f6a08ccaba.png
或者是这样
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值