在 JavaScript 中,异步操作是非常常见的。我们经常需要执行一些耗时的任务,例如发送网络请求、读取文件、定时器等,这些任务都是异步的,不会阻塞主线程的执行。为了有效地处理这些异步操作,并保持代码的可读性和可维护性,JavaScript 提供了多种异步处理的方法。今天我们将来聊聊js异步处理的发展。
同步和异步的概念
同步(Synchronous)指的是按照代码的顺序一步一步地执行,每一步必须等待上一步完成后才能执行下一步。在同步操作中,程序会阻塞并等待当前操作完成才能继续执行后续的操作。这意味着如果有一个操作耗时很长或者发生阻塞,整个程序都会被阻塞,直到该操作完成。同步操作适用于简单的任务或者需要按照严格顺序执行的任务。
异步(Asynchronous)指的是在遇到耗时的操作时,不会等待该操作的完成,而是继续执行后续的操作。在异步操作中,程序不会被阻塞,而是继续执行其他的任务。当异步操作完成后,会触发一个回调函数或者返回一个 Promise 对象来处理操作的结果。异步操作适用于需要进行网络请求、文件读写、定时器等耗时操作的场景,以提高程序的性能和响应速度。
通过代码我们能很轻松的理解:
javascript
function a() {
setTimeout(()=>{
console.log('A');
},1000)
}
function b() {
console.log('B');
}
a()
b()
我们定义两个函数a和b,a中有一个setTimeout定时器,在一秒之后输出A,这是一个耗时操作,而函数b中只有一个打印B的命令,调用后立即执行,不是耗时操作。尽管我们是先调用a再调用b,但由于js代码是异步执行,则不会先执行需要耗时的a函数,而是先执行不需要耗时的b,结果如下:
如果是同步,则不管a函数耗不耗时,耗多长时间,都得按顺序先执行a再执行b。
总结起来,同步操作是按照顺序一步一步执行,必须等待上一步完成才能进行下一步;异步操作是不需要等待耗时操作的结果,可以继续执行其他任务,并通过回调函数或者 Promise 对象处理操作的结果。异步操作适用于需要响应速度和并发性的场景,而同步操作适用于简单的任务或者需要严格顺序执行的任务。
但是若是我们就是想让不耗时的b在耗时的a后面执行呢?
我们一起来看看js是如何处理的。
回调函数
在es6之前,处理异步的手段就是回调函数:
javascript
function a() {
setTimeout(()=>{
console.log('A');
b()
},1000)
}
function b() {
console.log('B');
}
a()
我们将b的调用放在打印A的命令后面,就能保证先输出A再输出B了:
但是回调函数这个办法有个很大的缺点,就是随着异步操作的嵌套增多,我们也容易陷入回调地狱:如果b也是耗时操作且所需时间依然比a短,要求b在a之后执行,这时再来个函数c,耗时比b短且需要在b之后执行,我们又需要把c函数的调用放在函数b中去,要是之后还有函数d,调用要放在函数c里,还有函数e、f、g...那么我们的代码的可读性和可维护性将变得非常差。
为了解决上述问题,es6引入了Promise对象
Promise
Promise异步处理如下,我们边看代码边解释:
1. promise 实例对象后面可以接 .then() ,then中回调的执行取决于promise中的resolve有没有生效
javascript
function a() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('A');
resolve()
},2000)
})
}
function b() {
setTimeout(()=>{
console.log('B');
},1000)
}
a().then(()=>{
b()
})
我们还是定义了两个函数a和b,b函数中的操作要比a耗时短,但我们需要先执行a再执行b。我们在函数a中通过Promise 构造函数创建一个 Promise 对象并将其返回。构造函数接受一个 executor 函数作为参数,该函数包含两个回调函数 resolve 和 reject,分别表示异步操作成功和失败时要执行的逻辑。当函数里面的异步操作'两秒后输出A'执行后,就调用resolve方法。
我们在a的调用后接上.then()方法,把b的调用放在里面,当a中Promise的resolve调用后,.then()中的回调开始生效执行,而A的输出的命令在resolve调用之前就已经完成,所以B会在A之后输出。
2. resolve(参数) 参数会传递给then中的回调函数
如果resolve()是有参数的,则参数会传递给then中的回调函数:
javascript
function a() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('A');
resolve('hello')
},2000)
})
}
function b() {
setTimeout(()=>{
console.log('B');
},1000)
}
a()
.then((res)=>{
console.log(res);
b()
})
我们给resolve加上参数hello
,then中的回调函数可以接收到,在里面加上打印这个参数的命令,结果是:
3. then 方法会默认返回promise对象,所以then2可以接在then的后面,当then当中有人为返回的新的promis对象时,then就将人为返回的promise对象作为唯一返回值,那么then2就相当于接在人为返回的promise对象后。
javascript
function a() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('A');
resolve()
},2000)
})
}
function b() {
return new Promise((resolve)=>{
setTimeout(()=>{
console.log('B');
resolve()
},1000)
})
}
function c(){
console.log('C');
}
a()
.then(()=>{
return b()
})
.then(()=>{
c()
})
我们新加了个函数c,要求在执行函数b之后再执行a。then方法可以接在a()之后是因为它会返回一个Promise对象,而第二个then可以接在第一个then后面是因为then方法会默认返回一个Promise对象,此时我们需要在第一个then里将b的调用return一下,返回新的b的Promise对象,不然之后的第二个then方法会跟在第一个then默认的promise对象上,而不是b的Promise对象上,这样会导致c没有跟在b后面,会让c先执行:
所以一定要注意,连续使用then时,一定不要忘记将上一个then中的返回的Promise对象return。
实际上,当异步操作嵌套很多时,尽管Promise比回调函数美观很多,但它依然也会有一大串,于是在es7,又有了async-await。
async-await
async-await 关键字,进一步简化了异步处理的代码。async/await 是基于 Promise 的语法糖,使得异步代码看起来更像同步代码,同时保持了异步非阻塞的特性。
javascript
function a() {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('A');
resolve()
},2000)
})
}
function b() {
return new Promise((resolve)=>{
setTimeout(()=>{
console.log('B');
resolve()
},1000)
})
}
function c(){
console.log('C');
}
async function foo(){
await a()
await b()
c()
}
foo()
我们仍然需要和之前一样用Promise。之后使用 async-await,将异步操作包装在函数foo中,并使用 await 关键字等待 Promise 对象的解析。await 关键字会暂停异步函数的执行,直到 Promise 对象成功解析或失败为止。await a()会让之后的代码等待a执行完毕之后再去执行。
这样,就让异步处理的代码变得非常整洁美观啦!
本文的知识到这就结束啦,欢迎下次再来一起学习ヾ(◍°∇°◍)ノ゙!!