目录
同步&异步的概念
js中异步的应用场景
实现异步的四种方法
1、 回调函数
2、Promise
3、Generator
4、 async/await
「异步编程」是前端工程师日常开发中经常会用到的技术,也是校招面试过程中常考的一个知识点。
通过掌握「异步编程」的四种方式,可以让我们能够更好地处理JavaScript中的异步操作,提高代码的性能和用户体验。
因此,「今天就想和大家来聊聊JS异步编程的四种方式!」
同步&异步的概念
在讲这四种异步方案之前,我们先来明确一下同步和异步的概念:
所谓「同步(synchronization)」,简单来说,就是「顺序执行」,指的是同一时间只能做一件事情,只有目前正在执行的事情做完之后,才能做下一件事情。
「同步操作的优点」在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问题。
「同步操作的缺点」在于「会阻塞后续代码的执行」。如果当前执行的任务需要花费很长的时间,那么后面的程序就只能一直等待。
所谓「异步(Asynchronization)」,指的是当前代码的执行不影响后面代码的执行。当程序运行到异步的代码时,会将该异步的代码作为任务放进「任务队列」,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务即可。
因此,「异步操作的优点就是:不会阻塞后续代码的执行。」
js中异步的应用场景
开篇讲了同步和异步的概念,那么在JS中异步的应用场景有哪些呢?
-
「定时任务」:setTimeout、setInterval
-
「网络请求」:ajax请求、动态创建img标签的加载
-
「事件监听器」:addEventListener
实现异步的四种方法
对于「setTimeout、setInterval、addEventListener」这种异步场景,不需要我们手动实现异步,直接调用即可。
但是对于「ajax请求」、「node.js中操作数据库这种异步」,就需要我们自己来实现了~
1、 回调函数
在微任务队列出现之前,JS实现异步的主要方式就是通过「回调函数」。
以一个简易版的Ajax请求为例,代码结构如下所示:
function ajax(obj){
let default = {
url: '...',
type:'GET',
async:true,
contentType: 'application/json',
success:function(){}
};
for (let key in obj) {
defaultParam[key] = obj[key];
}
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async);
xhr.send();
xhr.onreadystatechange = function (){
if (xhr.readyState === 4){
if(xhr.status === 200){
let result = JSON.parse(xhr.responseText);
// 在此处调用回调函数
defaultParam.success(result);
}
}
}
}
我们在业务代码里可以这样调用「ajax请求」:
ajax({
url:'#',
type:GET,
success:function(e){
// 回调函数里就是对请求结果的处理
}
});
「ajax请求」中的success方法就是一个回调函数,回调函数中执行的是我们请求成功之后要做的进一步操作。
这样就初步实现了异步,但是回调函数有一个非常严重的缺点,那就是「回调地狱」的问题。
大家可以试想一下,如果我们在回调函数里再发起一个ajax请求呢?那岂不是要在success函数里继续写一个ajax请求?那如果需要多级嵌套发起ajax请求呢?岂不是需要多级嵌套?
如果嵌套的层级很深的话,我们的代码结构可能就会变成这样:
因此,为了解决回调地狱的问题,提出了「promise」、「async/await」、「generator」的概念。
2、Promise
「Promise」作为典型的微任务之一,它的出现可以使JS达到异步执行的效果。
一个「Promise函数」的结构如下列代码如下:
const promise = new Promise((resolve, reject) => {
resolve('a');
});
promise
.then((arg) => { console.log(`执行resolve,参数是${arg}`) })
.catch((arg) => { console.log(`执行reject,参数是${arg}`) })
.finally(() => { console.log('结束promise') });
如果我们需要嵌套执行异步代码,相比于回调函数来说,「Promise」的执行方式如下列代码所示:
const promise = new Promise((resolve, reject) => {
resolve(1);
});
promise.then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
return value * 2;
}).then((value) => {
console.log(value);
}).catch((err) => {
console.log(err);
});
即通过then来实现多级嵌套(「链式调用」),这看起来是不是就比回调函数舒服多了~
每个「Promise」都会经历的生命周期是:
-
进行中(pending) :此时代码执行尚未结束,所以也叫未处理的(unsettled)
已处理(settled) :异步代码已执行结束 已处理的代码会进入两种状态中的一种:-
已拒绝(rejected):遇到错误,异步代码执行失败 ,由reject()触发
-
已完成(fulfilled):表明异步代码执行成功,由resolve()触发
-
因此,「pending」,「fulfilled」, 「rejected」就是「Promise」中的三种状态啦~
需要注意的是,在「Promise」中,要么包含resolve() 来表示 「Promise」 的状态为fulfilled,要么包含 reject() 来表示「Promise」的状态为rejected。
不然我们的「Promise」就会一直处于pending的状态,直至程序崩溃...
除此之外,「Promise」不仅很好的解决了链式调用的问题,它还有很多高频的操作:
-
·Promise.all(promises) :接收一个包含多个Promise对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略
-
·Promise.allSettled(promises) : 接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:'fulfilled' // 或 'rejected', value // 或reason}
-
·Promise.race(promises) : 接收一个包含多个Promise对象的数组,等待第一个有结果(完成/拒绝)的Promise,并把其result/error作为结果返回
示例代码如下所示:
function getPromises(){
return [
new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),
new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),
new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),
];
}
Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);
打印结果为:
3、Generator
「generator」是ES6提出的一种异步编程的方案。因为手动创建一个iterator十分麻烦,因此ES6推出了「generator」,用于更方便的创建iterator。
也就是说,「generator」就是一个返回值为iterator对象的函数。
在讲「generator」之前,我们先来看看iterator是什么:
❝iterator中文名叫「迭代器」。它为js中各种不同的数据结构(Object、Array、Set、Map)提供统一的访问机制。
❞
任何数据结构只要部署iterator接口,就可以完成遍历操作。
因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。
我们通过一个例子来看看generator的特征:
function* createIterator() {
yield 1;
yield 2;
yield 3;
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
形式上,「generator」 函数是一个普通函数,但是有两个特征:
-
·function关键字与函数名之间有一个星号
-
·函数体内部使用yield语句,定义不同的内部状态
在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。
但在「generator」中,就打破了这几点,所以「generator」和普通的函数完全不同。
当以function* 的方式声明了一个「generator」生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的「generator」,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。
function* generator() {
yield 1
yield 2
};
let iterator = generator();
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: undefined, done: true}
4、 async/await
最后我们来讲讲「async/await」,终于讲到这儿了!!!
「async/await」是ES7提出的关于异步的终极解决方案。我看网上关于「async/await」是谁的语法糖这块有两个版本:
-
第一个版本说「async/await」是Generator的语法糖
-
第二个版本说「async/await」是Promise的语法糖
其实,这两种说法都没有错。
「关于async/await是Generator的语法糖:」
所谓generator语法糖,表明的就是「aysnc/await」实现的就是generator实现的功能。但是「async/await」比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。
而async配合await得到的就是断点执行后的结果。因此「async/await」比generator使用更普遍。
「关于async/await是Promise的语法糖:」
如果不使用「async/await」的话,Promise就需要通过链式调用来依次执行then之后的代码:
function counter(n){
return new Promise((resolve, reject) => {
resolve(n + 1);
});
}
function adder(a, b){
return new Promise((resolve, reject) => {
resolve(a + b);
});
}
function delay(a){
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a), 1000);
});
}
// 链式调用写法
function callAll(){
counter(1)
.then((val) => adder(val, 3))
.then((val) => delay(val))
.then(console.log);
}
callAll();//5
虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。
而「async/await的出现,就使得我们可以通过同步代码来达到异步的效果」:
async function callAll(){
const count = await counter(1);
const sum = await adder(count, 3);
console.log(await delay(sum));
}
callAll();// 5
由此可见,「Promise搭配async/await的使用才是正解!」