一、什么是异步、同步
在讲解同步、异步之前首先要了解几个关键概念:
1.单线程:线程是操作系统调度的基本单位,在同一个时间内的一个线程中只会执行一个任务。JavaScript 是一门单线程的编程语言,这就意味着该他只有一个主线程执行当前代码,就会按照代码顺序进行执行。就可以简单的理解成一条工厂流水线,只有当上一道工序执行完之后才会进行下一步工序。
2.任务队列:任务列表是 JavaScript 实现异步的基础,在代码执行的时候,如果遇到异步操作(定时器、事件请求、事件监听等)时,会将相应的回调函数按照特定的顺序进行排列添加到任务队列中,等待事件循环进行处理。
3.事件循环:事件循环是实现异步编程的核心机制,它控制着代码的执行顺序和异步任务的处理。事件循环根据特定执行顺序不断地从任务队列中获取任务并将其交给主线程执行。
4.同步任务和异步任务:同步任务是按照顺序在主线程上执行的任务,执行一个任务时会阻塞后续任务的执行。异步任务是在主线程上注册并在将来某个时间点执行的任务,执行异步任务时不会阻塞后续任务的执行。这里借用一个
5.微任务和宏任务:JavaScript 中的任务可以分为微任务和宏任务。微任务是在当前任务执行完毕后立即执行的任务,它们使用微任务队列进行管理。而宏任务是在事件循环的下一轮中执行的任务,它们使用任务队列进行管理。
二、为什么会有异步、同步
由于 JavaScript 是单线程编程,所以在执行的时候,如果遇到执行时间较长或者等待时间较长的时候,就会导致接下来的代码没来得及执行,从而影响效果。异步编程模型的出现是为了处理那些耗时的操作,比如网络请求、文件读写、定时器等。在进行这些操作时,如果使用同步方式,会导致程序在等待操作完成时被阻塞,无法执行其他任务,从而影响用户体验或整体性能。而异步编程允许在进行这些耗时操作时,立即返回并继续执行后续的任务,不会阻塞程序的执行。当异步操作完成后,会触发相应的回调函数或执行注册的事件处理函数,来处理操作的结果。这样可以充分利用 CPU 的资源,提高代码的执行效率,提升用户的体验效果。
三、事件循环
主线程:JS的首要任务,可以理解为同步任务,先进先出;
执行栈:执行栈是一个先入后出的数据结构,一个同步任务来了进入栈底马上执行它,然后释放;
任务队列:任务队列的存在是为了应付一些异步任务,在执行一个任务如何迟迟等不到不能一直等待,必须让程序接着往下进行;任务队列用于存放这些任务;先进先出;
宏任务:较常见的:script全部代码、setTimeout、setInterval、setImmediate、I/O、UI Rendering
微任务:较常见的:Process.nextTick、Promise(new Promise的过程属于同步任务,resove或者reject之后才算微任务)
Event Loop的流程:
1. JavaScript 引擎首先执行同步任务,将任务在主线程中按顺序执行。
2. 当遇到异步任务时,引擎将该任务移出主线程,提交给对应的异步进程进行处理,并注册对应的回调函数。
3. 异步任务完成后,会将其回调函数添加到任务队列中。
4. 当主线程任务栈为空时,事件循环会检查任务队列。
5. 如果任务队列中有任务,事件循环将按照微任务——>宏任务的顺序先将微任务取出,并添加到任务栈中执行。
6. 当微任务执行完毕后,事件循环再次检查任务队列。
7. 如果任务队列中还有任务,微任务执行完毕后,事件循环会取出一个当前的宏任务并添加到任务栈中执行。
8. 因为每个宏任务中还可能会存在其余的微任务和宏任务,此时就会重复上述步骤,不断循环检查任务队列和执行任务,直到任务队列为空。
四、处理异步的五种方式
异步编程的方法大概有:回调函数、发布/订阅、Promise对象、Generator函数、async/await,注意Promise对象是在ES6之前由社区提出的,ES官方参考了大量的异步场景,在ES6的时候总结出了一套异步通用的标准模型;
1、回调函数
回调函数是异步编程最基本的方法,回调函数是一段可执行的代码段,它作为一个参数传递给其他代码,其作用是在需要的时候方便调用这段代码(回调函数)。简单的理解为事件2是事件1的参数,当在执行事件1的时候,在某个时间点我需要事件2执行的结果,就会执行事件2。同样事件2也可以有另外一个事件3作为参数在某个时间段需要事件3的执行结果…
这里看一段代码:
function fn1(a, b, c, callback1){
return a + callback1(b, c, fn3);
}
function fn2( b, c, callback2){
return b + callback2(c);
}
function fn3(c){
return c;
}
console.log(fn1(1, 2, 3, fn2));
如上述代码,事件1有一个参数是事件2,事件2有一个参数是事件3…当执行的事件较多的时候就会产生回调地狱的问题,这里进行一下总结:
优点:
- 提高代码的复用性和灵活性:回调函数可以将一个函数作为参数传递给另一个函数,从而实现模块化编程,提高代码的复用性和灵活性。
- 解耦合:回调函数可以将不同模块之间的关系解耦,使得代码更易于维护和扩展。
- 可以异步执行:回调函数可以在异步操作完成后被执行,这样避免了阻塞线程,提高应用程序的效率。
缺点:
- 回调函数嵌套过多会产生回调地狱,导致代码难以维护:如果回调函数嵌套层数过多,代码会变得非常复杂,难以维护。
- 回调函数容易造成竞态条件:如果回调函数中有共享资源访问,容易出现竞态条件,导致程序出错。
- 代码可读性差:回调函数的使用可能会破坏代码的结构和可读性,尤其是在处理大量数据时。
小结:代码灵活、易于扩展,但是不易于阅读、容易出错。
竞态条件:当一段代码被重复执行的的时候,先执行的不一定先返回。比如在进行一个分页请求数据的时候,连续两次点击下一页,此时分页器显示第三页,但是由于网络等原因导致第二页的数据返回的时间晚于第三页返回的时间,就会导致分页器展示第三页,但是实际展示的数据是第二页的数据。
2、发布/订阅模式
发布/订阅模式是23中设计模式中的一种,可以理解为对报纸的订购,需求方提出订购,提供方根据需求方的信息唯一值进行发布。下面看一段代码:
// 创建一个类
class OrderPublication {
constructor() {
this.events = {}; // 存储所有的事件和对应的处理函数。
}
// 订阅事件,将订阅的订阅名名和事件进行保存
on(eventName, handler) {
// 判断当前是否有该订阅名,如果没有的话,首先先创建该订阅名
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 向对应的订阅名里添加事件
this.events[eventName].push(handler);
}
// 取消订阅事件,将订阅的订阅名下的对应事件删除
off(eventName,handler){
// 1
this.events[eventName] = []
// 2
if(this.events[type]){
this.events[type].splice(this.events[eventName].indexOf(handler),1);
}
console.log("解除成功");
}
// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((handler) => handler(data));
} else {
console.log("没有该事件")
}
}
}
// 床技安实例对象
const em = new OrderPublication();
// 在需要数据的地方,订阅事件
em.on("event", function (data) {
console.log("event 发生了, 数据为:", data);
});
// 模拟异步加载数据
function loadData() {
setTimeout(() => {
const data = {message: 'Hello world'};
em.emit("event", data);
}, 1000);
}
// 加载数据
loadData();
// 取消订阅
em.off("event")
关于订阅与发布模式,前端框架 vue2.x 版本的响应式设计就使用到了这种方法,这里可以看另一博主的详细讲解 Vue响应式原理探究之“发布-订阅”模式,订阅与发布模式 关于订阅与发布模式不仅可以用来处理异步,他还有很多的三方库用来处理组件间的数据传输。 react中利用订阅与发布处理组件间相互通信
3、Promise
Promise 是异步编程的一种解决方案,并且是目前使用最多的一种方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。在进行异步处理时,promise会通过 .then 配合 .catch 链式调用处理。当状态为 Resolved 会执行 .then 的函数,在进行链式调用的时候,一般会在最后使用 .catch 进行兜底处理,无论在前边那一步的调用,只要遇到 Rejected 状态时,就会指向 .catch 中的事件。
(1)Promise 的实例有三个状态:
- Pending(进行中)
- Resolved(已完成)
- Rejected(已拒绝)
当把一件事情交给 promise 时,它的状态就是 Pending,任务完成了状态就变成了 Resolved、没有完成失败了就变成了 Rejected。
(2)Promise 的实例有两个过程:
- pending -> fulfilled : Resolved(已完成)
- pending -> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
(3)Promise 的特点:
对象的状态不受外界影响。promise 对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是 promise 这个名字的由来——“承诺”;一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled,从pending 变为 rejected。这时就称为 resolved(已定型)。如果改变已经发生了,你再对 promise 对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。
(4)Promise 的缺点:
无法取消 Promise,一旦新建它就会立即执行,无法中途取消。如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
(5)常用的的API
- Promise.all(promises)
Promise.all 的参数是多个 promise 对象组成的数组,返回的也是一个 promise 对象,并且只有参数数组中的所有对象状态都为 resolve 的时候当前返回值状态才为 Resolved ;
Promise.race 的参数也是多个 promise 对象组成的数组,返回的也是一个 promise 对象,但与 Promise.all 不同的是,该返回的 promise 的状态是参数数组中第一个返回的状态,如果第一个状态返回的是 resolve ,与之后其余参数的状态无关。
- Promise.allSettled(promises):
Promise.allSettled接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:‘fulfilled’ // 或 ‘rejected’, value // 或reason}
-
Promise.resolve(value)
-
Promise.reject(reason)
(6)总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的
下面看一段代码:
function p1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(res+'a')
},1000)
})
function p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(res+'b')
},1000)
})
function p3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(res+'c')
},1000)
})
const pp = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('a')
},1000)
}).then(res=>{
console.log('res1',res) //1秒后打印 a
return p1
}).then(res=>{
console.log('res',res) //2秒后打印 aa
return p2
}).then(res=>{
console.log('res3',res) //3秒后打印 aaa
return p3
}).catch(error =>{
// 这一步用来尽享兜底处理,上边只要有异步遇到问题,就会在这里进行处理
console.log(error, "哈哈哈哈,出问题了")
})
4、生成器函数 Generator/yield
Generator 是ES6提出的一种新的异步编程的方案,通过 yield 关键字来控制代码的暂停,Generator 函数使用时跟普通函数几乎是一样的,其中有两个不同点,function关键字与函数名之间有一个星号 函数体内部使用yield语句,定义不同的内部状态。
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。下面看一段代码:
function* createGenerator() {// 步骤一
console.log(1);
yield 1;
console.log(2);
yield 2;
console.log(3);
yield 3;
console.log(4);
}
// createGenerator 可以像正常函数一样被调用,但是并不会执行
let generatorItem = createGenerator();
console.log(generatorItem.next().value); // 1
console.log(generatorItem.next().value); // 2
console.log(generatorItem.next().value); // 3
这是打印 generatorItem 看一下
当我们执行该 Generator 函数时,会生成一个遍历器对象(Iterator Object),此时可以理解为该函数执行到并且暂停在了 步骤一(看代码) 的位置,此时会将该线程的执行权放出去给下之后的代码执行,在该函数的返回值中会有一个 next() 函数,
next()函数是用来控制代码的执行和暂停,每执行一次,就会返回下一个 yield 的值,直到执行完为止,如下:
5、async和await函数
async/await 其实是 Generator 的语法糖,它能实现的效果都能用 then 链来实现,它是为优化 then 链而开发出来的。从字面上来看,async 是“异步”的简写,await 则为等待,所以很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定 await 只能出现在 asnyc 函数中,先来看看 async 函数返回了什么:
所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda 表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:那如果 async 函数没有返回值,又该如何?很容易想到,它会返回Promise.resolve(undefined)。联想一下 Promise 的特点——无等待,所以在没有await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
注意:Promise.resolve(x) 可以看作是 new Promise(resolve =>resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
优势:
promise进行链式调用,达昂进行的异步执行语句较多时,会有非常长的链式调用,代码可读性非常差,使用async/awite进行时,可以简化代码书写,提高代码可读性;
关于async/await 是如何执行的,这里看一段代码:
function resolveAfter2Seconds1 () {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved1');
}, 1000);
});
}
function resolveAfter2Seconds2 () {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved2');
}, 1000);
});
}
function resolveAfter2Seconds3 () {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved3');
}, 1000);
});
}
async function asyncCall () {
let time = new Date();
console.log('a', new Date() - time);
const result1 = await resolveAfter2Seconds1();
console.log(result1); // 拿到的是正确返回的结果
console.log('b', new Date() - time);
const result2 = await resolveAfter2Seconds2();
console.log(result2);
console.log('c', new Date() - time);
const result3 = await resolveAfter2Seconds3();
console.log(result3);
console.log('d', new Date() - time);
}
asyncCall();
执行结果如下:
JavaScript中的异步_js异步-CSDN博客