异步编程

概念

同步:一定要等任务执行完了,得到结果,才执行下一个任务。(类似于串行)

1
A -> B -> AJAX 请求 -> C ---------------------------

异步:不等任务执行完,直接执行下一个任务。(类似于并行)

1
2
A -> B -> C ---------------------------------------
-> AJAX 请求 --------------------------------

当各个任务相互独立的时候,可以使用异步的方式执行,可以加速运行效率

1
2
3
4
5
6
7
8
console.log("A")

setTimeout(() => {
console.log("B")
}, 1000)

console.log("C")
// 输出顺序是 ACB

回调函数

回调函数可以告诉异步任务下一步做什么。

当A B两个函数存在运行顺序的时候,可以使用嵌套回调函数进行维护。比如我们希望先打印A,过了一秒钟之后再打印B

1
2
3
4
5
6
7
8
9
10
11
12
13
function A() {
console.log("A")
}
function B() {
console.log("B")
}

setTimeout(() => {
A()
setTimeout(() => {
B()
}, 1000)
}, 1000)

但是当需要顺序运行的函数非常多时,也会导致嵌套过多,导致代码横向的增长,不利于代码理解与维护,这也是JS中常提到的“回调地狱”。

同时,嵌套函数存在耦合性,修改起来很麻烦

Promise

构造Promise对象:

1
2
3
var a = new Promise((resolve, reject) => {
// 下一步
})

resolve 和 reject 都是函数,调用 resolve 代表一切正常,调用reject 代表出现异常

****三种状态

  • Pending 待定:最初始状态
  • fulfilled 解决:执行了resolve()函数后,状态变为fulfilled,并执行then里的方法
  • rejected 拒绝:执行了reject()函数后,状态变为rejected,并执行catch里的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(a) // pending
resolve(111)
setTimeout(() => { // fulfilled
console.log(a)
})
})
})

var b = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(b) // pending
reject(111)
setTimeout(() => { // rejected
console.log(b)
})
})
})

在相应的函数中执行完了之后,Promise的状态还会改变

**then(): **

  • 正常返回的时候,Promise的状态是fulfilled
  • 报错的时候,Promise的状态是rejected
1
2
3
4
5
6
7
8
9
10
var a = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(a) // pending
resolve(111)
})
}).then(data => {
// return 1; a的状态为 fulfilled
throw new Error("报错") // a的状态为 rejected
})

catch():

  • 正常返回的时候,Promise状态是fulfilled,可以继续往下执行then函数
  • 报错的时候,Promise状态是rejected

由于then块默认会向下顺序执行,return无法将其中断,只能通过throw来跳转至catch实现中断。

总结

  • Promise 对象是异步编程的一种解决方案,一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。
  • 实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
  • 状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态
  • resolve()执行后,调用then方法;reject()调用catch方法

async / await 异步函数

概念理解

  • 执行异步函数async函数,返回的都是Promise对象
  • 在异步函数中使用await指令,可以简化Promise、then这一系列过程,async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码
  • 在await 指令后必须跟一个 Promise
1
2
3
4
5
6
7
async function f1() {
const a = await Promise.resolve(1);
console.log(a)
}

f1()
console.log("2") // 输出顺序为 2 1
  • 使用try…catch来捕获await抛出的异常
1
2
3
4
5
6
7
8
9
10
11
async function f1() {
try {
const a = await Promise.reject(3);
console.log(1)
} catch(e) {
console.log(e)
}
}

f1()
console.log("2") // 输出 2 3

await阻塞示例

在下面的代码中,await等待了一个Promise对象的返回,并在等待的过程中阻塞了后面的代码。

console.log("start")、test1() 、console.log("end")是视为同步代码进行的

1
2
3
4
5
6
7
8
9
10
11
12
13
async function test1() {
try {
console.log(11) // 2
const a = await Promise.resolve(22)
console.log(33) // 4
} catch(e) {
console.log(e)
}
}

console.log("start") // 1
test1()
console.log("end") // 3

在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。

1
2
3
4
5
6
7
8
9
10
async function test1() {
try {
console.log(11)
const a = Promise.resolve(22)
// 删除了await,立即返回一个promise对象 下一行不会被阻塞
console.log(33)
} catch(e) {
console.log(e)
}
}

async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余

应用举例

假如函数B需要使用函数A返回的值来进行调用,但可能由于受到网速的影响使得B被调用时还没有拿到A的返回值,导致调用失败,这时候也需要异步处理。

1
2
3
4
5
6
7
8
function A() {
setTimeout(() => {
return 3;
}, 1000)
}

var res = A() // 调用完A之后直接打印 出现了错误
console.log(res) // undefined

Event Loop

Event Loop是什么

在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。

Event Loop执行顺序

  • 先顺序执行同步代码、宏任务
  • 同步栈为空,查询是否有异步代码需要执行
  • 循环访问callback队列,执行所有微任务
  • 执行完,是否需要渲染页面