JS中的同步与异步
在JavaScript中,同步(Synchronous)和异步(Asynchronous)是决定代码执行方式的两个基本概念,主要涉及到代码执行的时序和是否阻塞主线程。
同步(Synchronous) : 同步执行意味着代码按照书写顺序依次执行,每个任务必须等待前一个任务完成后才能开始。在同步执行模式下,程序遵循从上到下的顺序执行,如果某个任务耗时较长(如文件读写、网络请求等),那么后续的代码必须等待这个任务完成才能继续执行,如果该任务等待时间过长,那么其他的代码将无法执行,会导致阻塞,这可能会导致用户界面冻结或程序响应迟缓。
异步(Asynchronous) : 与同步相反,异步执行允许程序在等待某个操作(如I/O操作、网络请求等)完成的同时,继续执行后续的代码,而不需要阻塞。这意味着异步任务不会立即得到结果,而是通过其他的方式在未来的某个时间点通知结果。异步任务会被放入任务队列中,当主线程上的同步任务执行完毕,事件循环会从任务队列中取出任务来执行,这样就实现了非阻塞的执行环境,提高了程序的响应性。
下面这段代码就是一个简单的同步与异步操作:
console.log('1111')
setTimeout(()=>{
console.log('1234')
},3000)
setTimeout(()=>{
console.log('3333')
},3000)
setTimeout(()=>{
console.log('4444')
},5000)
console.log('2222')
JS中的Event Loop
由于JavaScript是单线程的语言,所以其中的程序遵循从上到下的顺序执行,这将很容易发生阻塞,所以为了解决单线程执行异步任务的问题,JS引入了一种独特的机制——Event Loop(事件循环)。这一机制不仅保证了代码执行的有序性,还使得JavaScript能够高效地处理异步操作,如网络请求、定时器等。其核心概念包括:
- 宏任务(Macro Task) :如script整体执行、setTimeout、setInterval等。
- 微任务(Micro Task) :如Promise的回调、MutationObserver等,它们比宏任务有更高的优先级。
- 执行流程:首先执行全局脚本(宏任务),然后检查微任务队列并立即执行所有微任务,接着回到宏任务,如此循环往复。
异步任务在触发时被注册登记到Event Table,当主线程空闲(即执行栈为空)且当前宏任务执行完毕后(同步任务执行完毕),系统会从Event Table中取出事件放入Event Queue(事件队列)。Event Loop不断地检查Event Queue,将队列中的事件(任务)按序分配给主线程执行。如果异步任务中还有异步任务,那将循环该操作,直到Event Table为空。
利用该机制我们能很容易的理解这段代码的执行过程以及是说出它的输出结果:
console.log('1111')
setTimeout(()=>{
console.log('1234')
},3000)
setTimeout(()=>{
console.log('3333')
},3000)
setTimeout(()=>{
console.log('4444')
},5000)
console.log('2222')
这段代码会将同步的任务快速执行,输出1111和2222,而异步任务setTimeout()将被注册登记到Event Table中,等当前宏任务执行完毕后(同步任务执行完毕)且主线程空闲(即执行栈为空),系统会从Event Table中取出事件放入Event Queue队列中,Event Loop不断地检查Event Queue,将队列中的事件(任务)按序分配给主线程执行。
来看看该过程的流程图,更加便于记忆和理解:
回调函数与Promise
-
回调函数:最初解决异步问题的方法是使用回调函数,即将一个函数作为参数传递给另一个函数,在特定时机执行。但随着异步操作增多,容易形成“回调地狱”(即多层嵌套的回调函数),代码可读性和维护性大大降低。
function a (x,y,z){ x(y,z); } function b (y,z){ y(z); } function c (z){ z() } function d (){ console.log("abcd") } a(b,c,d)这就是回调函数,将
bcd函数作为参数传给a函数,然后又一层一层嵌套,之后才能输出abcd,这也是“回调地狱”(即多层嵌套的回调函数) -
Promise:为了解决回调地狱问题,ES6引入了Promise,这是为了更加优雅的处理异步操作及其结果。
function xq() {// pending resloved rejected return new Promise((resolve, reject) =>{ setTimeout(() =>{ console.log('相亲') resolve(); },2000) }) } function marry() { return new Promise((resolve, reject) =>{ setTimeout(()=>{ console.log('结婚') resolve(); },1000) }) } xq() .then(()=>{ // promise里的status状态从penging到resloved,才能调用执行 marry() })这是Promise的回调调用
Promise
-
Promise的过程通常包括以下几种状态:
- Pending(等待中):初识状态,异步任务还在等待中,还没有完成;
- Resolved(已完成/已处理):异步操作成功完成;
- Rejected(已拒绝/失败):异步操作失败。
-
Promise的作用:
- 提供了一种更清晰、更结构化的方式来处理异步操作,避免了回调地狱。
- 通过调用
.then方法可以方便地进行链式调用,依次处理多个异步操作。 - 能够更好地处理错误,通过调用
.catch方法统一捕获异步操作中的错误。
-
.then方法的调用:只有promise的实例对象可以接.then(),且当promise里的status状态从pending到resloved,才能调用执行,所以函数里头用了Promise都会调用一个resolve()。
function baby(){
console.log('生小孩')
}
xq()
.then(()=>{
marry()
})
.then(()=>{
baby()
})
在上面那段代码中的俩个函数后,再加入这个函数,并且调用。很清楚的看到其中的链式调用,xq.then(...).then(...)使得这三个异步任务捋成同步任务,链式串联在一起,后面的任务要等前面的任务完成并返回Promise的结果状态,之后再执行,根据代码的结果,也就是相亲成功之后再结婚,结婚成功之后再生小孩,这样就有效的处理了异步任务,使得异步操作的逻辑更加清晰易读且易于维护。
不过要注意的是,虽然.then方法支持链式调用,因为.then默认也会返回一个promise对象,但是状态默认是pending,这就会导致后面的.then用不上前面.then的状态,从而继续往前查找,也就是xq.then(...).then(...)中,相亲的状态是resloved,结婚就可以从pending变为resloved,但是虽然结婚是resloved,.then返回的状态是pending,导致后面接着的.then认为结婚是pending,然后继续往前查找,找到相亲的状态是resloved,这样才会执行。这样子会导致异步操作的顺序出问题,毕竟生小孩是在结婚的后面,所以这里我们需要在.then中返回一个promise对象,会覆盖掉.then自带的返回,这样就可以保证生小孩一定会在结婚之后了。
function xq() {// pending resloved rejected
return new Promise((resolve, reject) =>{
setTimeout(() =>{
console.log('相亲')
resolve();
},2000)
})
}
function marry() {
return new Promise((resolve, reject) =>{
setTimeout(()=>{
console.log('结婚')
resolve();
},1000)
})
}
function baby(){
console.log('生小孩')
}
xq()
.then(()=>{
return marry()
})
.then(()=>{
baby()
})
结语
JavaScript的Event Loop机制是其异步编程的核心,而Promise作为现代异步编程的重要工具,极大地改善了异步代码的可读性和可维护性。掌握和理解这些基础知识,对于编写高效、健壮的JavaScript应用至关重要。随着async/await语法的引入,进一步简化了异步编程模型,但其底层仍然是基于Promise和Event Loop机制。所以当我们能更好的理解底层知识,也就能更快地对新技术上手。