Async JavaScript

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:July 22 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。


目录

有特定需要的内容直接跳转到相关章节查看即可。

    Section 1. JavaScript 运行机制

    Section 2. 定时器

    Section 3. Callback

    Section 4. Promise

    Section 5. async 和 await

1. JavaScript 运行机制[1]

    JS 是单线程运行的,意思是在 JS 脚本运行过程中,只有一个 主线程(Main Thread)。如上图所示,主线程上按顺序执行 同步任务(Synchronous Task)。当碰到 异步任务(Asynchronous Task) 时,主线程不会推迟后面的其它同步任务,而是会继续执行下面的同步任务。当异步任务执行完成时,会向 事件队列(Event Queue) 中 push 一个事件(e.g. 磁盘读取完成、网络发送完成等)。当主线程运行完所有的同步任务后,就会循环地检测事件队列中放置的事件,包括了之前推迟处理的异步任务,以及新到来的异步任务(i.e. 鼠标、键盘输入,按下按钮等)。

    对于事件队列中的每一个异步任务所对应的事件,我们都需要关联一个 回调函数(callback function)。这样一来,当主线程开始处理事件的时候,实际上执行的就是回调函数中的内容。

2. 定时器

    定时器是一种特殊的异步任务,我们首先看它的形式:

1
2
3
4
5
// setTimeout() 是属于 window 的方法
// 传入参数:
// func: 回调函数
// 等待的毫秒数
setTimeout(func, duration)

    结合我们在 JavaScript 运行机制 中所描述的内容,setTimeout 异步任务会在到时间后向事件队列中插入对应的事件。但是我们知道,由于事件队列是一个 FIFO,在 setTimeout 的事件之前可能还有其它的未处理的异步事件。因此 setTimeout 的回调函数真正被执行的事件可能会超过它的第二个参数所约定的时间。

3. Callback

    为了能够实现保证异步任务运行顺序的程序行为,Callback 机制提供了一种方法。

1
2
3
4
5
6
7
8
9
10
11
function A(msg, callback){
let str = "I am another!"
console.log(`From A: ${msg}`);
typeof callback === function && callback(str);
}

function B(msg){
console.log(`From B: ${msg}`);
}

A("I am A!", B)

4. Promise[2]

    当我们嵌套多个 Callback 的时候,可以想像,整个程序的可读性将下降。

    那么如何解决按顺序执行多个异步程序呢?为了解决这个问题,JS 提供了一个叫做 Promise 的对象。Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。这个对象一共有三种状态:Pending 状态(进行中),Fulfilled 状态(已成功)和 Rejected 状态(已失败)。状态的转移只有两种情况:(1) Pending -> Fulfilled 和 (2) Pending -> Rejected。一旦状态改变就不会再改变。

    创建一个 promise 的构造函数如下:

1
2
3
4
5
6
7
8
9
var promise = new Promise(function(resolve, reject){
// ... some code

if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
})

    Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

    resolve 作用是将 Promise 对象状态由 "未完成" 变为 "成功",也就是 Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而 reject 函数则是将 Promise 对象状态由 "未完成" 变为 "失败",也就是 Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去

    Promise 实例生成后,可用 then 方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:

  1. Promise 对象状态改为 Resolved 时调用 (必选)
  2. Promise 对象状态改为 Rejected 时调用 (可选)

    例子如下所示:

1
2
3
4
5
6
function sleep(ms) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, ms);
})
}
sleep(500).then( ()=> console.log("finished"));

    请读者朋友结合我们上面的 JavaScript 运行机制 的内容,思考下面代码的执行顺序。

1
2
3
4
5
6
7
8
9
10
let promise = new Promise(function(resolve, reject){
console.log("AAA");
resolve()
});
promise.then(() => console.log("BBB"));
console.log("CCC")

// AAA
// CCC
// BBB

    执行后,我们发现输出顺序总是 AAA -> CCC -> BBB。表明,在 Promise 新建后会立即执行,所以首先输出 AAA。然后,then 方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以 BBB 最后输出。也就是说,then 的调用将会向事件队列中注册一个新的事件,当这个事件 dequeue 的时候,就执行回调函数中的内容。

    当发生错误的时候,我们也可以使用 .catch() 方法来处理 reject 的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
function A(msg){
return new Promise((resolve, reject) => {
let state = false;
if(state === false){
reject("something went wrong");
} else {
resolve(msg);
}
})
}

A().then((msg) => console.log(`finished: ${msg}`))
.catch((err) => console.log(`err: ${err}`));

    当我们有一连串想要按序执行的回调函数的时候,我们可以使用 Promise.all 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let p1 = new Promise((resolve, reject) => {
resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
resolve('success')
})

let p3 = Promise.reject('失败')

Promise.all([p1, p2]).then((result) => {
console.log(result) //['成功了', 'success']
}).catch((error) => {
console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 失败了,打出 '失败'
})

    Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值。

5. async 和 await [3]

    async 关键字可以用于声明一个函数是异步的,await 关键字则用于等待异步函数的返回。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}

asyncCall();

    注意!await 只能出现在 async 函数中。

附录:参考源

  1. 阮一峰, JavaScript 运行机制详解:再谈Event Loop
  2. 流眸Tel, JS执行——Promise
  3. 边城, 理解 JavaScript 的 async/await