# JavaScript教程 - 19 异步编程
# 19.1 异步和同步
什么是同步和异步?
同步就是代码一行一行执行,前一行不执行完,后一行不会执行。
但有时候某些操作(如网络请求、文件读取、定时器等)可能很慢,这时候如果继续同步执行,会“阻塞”后续代码执行,影响用户体验。
异步编程的出现就是为了解决这个问题:让程序能在等待某些操作完成的同时,继续做别的事。
举个栗子:
同步代码:
console.log('1');
console.log('2');
console.log('3');
// 输出顺序就是 1 -> 2 -> 3
2
3
4
5
- 上面的代码,一行一行按照顺序执行。
异步代码:
console.log('1');
setTimeout(() => {
console.log('2');
}, 1000);
console.log('3');
// 实际输出顺序是:1 -> 3 -> (等待1秒)-> 2
2
3
4
5
6
7
setTimeout
是一种异步方法,它不会阻塞主线程。
# 19.2 回调地狱
异步最常见的方式就是回调函数,举个栗子:
function doSomethingAsync(callback) {
setTimeout(() => {
console.log('执行任务...');
callback(); // 任务完成后再执行回调
}, 1000);
}
doSomethingAsync(() => {
console.log('回调被调用');
});
2
3
4
5
6
7
8
9
10
- 在上面的代码中,当任务执行完成后,会调用回调函数。
回调函数简单的还好,但是如果嵌套过多,会导致 回调地狱。
举个栗子:
我们模拟三个任务,每个任务耗时 1 秒,并且后一个任务需要用到前一个任务的结果:
function task1(callback) {
setTimeout(() => {
console.log("任务1");
callback("结果1");
}, 1000);
}
function task2(input, callback) {
setTimeout(() => {
console.log("任务2,接收到任务1的数据:", input);
callback("结果2");
}, 1000);
}
function task3(input, callback) {
setTimeout(() => {
console.log("任务3,接收到任务2的数据:", input);
callback("结果3");
}, 1000);
}
// 回调地狱开始:
task1(function (result1) {
// 任务1的结构result1传递给任务2
task2(result1, function (result2) {
// 任务2的结构result2传递给任务3
task3(result2, function (result3) {
console.log("所有任务完成,最终结果:", result3);
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- 虽然上面的任务是一个一个执行,逻辑是线性的,但代码却层层嵌套;所有逻辑耦合在一起,而且嵌套太深,看不清逻辑结构,这就是回调地狱。
那么如何解决回调地狱问题呢?
# 19.3 Promise
Promise 是 ES6 引入的一个构造函数,用于表示一个可能当前没值、但将来可能会返回值的对象,它用于管理异步操作的执行和结果,可以用来优化回调地狱。
# 1 Promise的使用
Promise
有三种状态:pending
(等待中)、fulfilled
(成功)、rejected
(失败)。
可以通过调用
resolve(value)
方法,将Promise
状态变为fulfilled
(成功),表示异步操作成功,并返回value
作为结果;可以通过调用
reject(error)
方法,将Promise
状态变为rejected
(失败),表示异步操作失败,并返回error
作为失败原因;
举个栗子:
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('成功结果');
} else {
reject('失败原因');
}
}, 1000);
});
2
3
4
5
6
7
8
9
10
11
- 可以使用
Promise
来执行异步任务,上面在异步任务执行成功后,使用resolve()
方法返回结果,如果执行失败,使用reject()
方法返回失败原因。 - 需要注意:Promise 在创建时就自动开始执行了。
那么如何获取 Promise 异步任务的返回结果呢?
使用 .then()
和 .catch()
处理,.then()
接收好消息,也就是 resolve(value)
方法传递的参数,.catch()
接收坏消息,也就是reject(error)
方法传递的参数。
promise.then((result) => {
console.log('成功:', result);
})
.catch((error) => {
console.error('失败:', error);
});
2
3
4
5
6
- 当调用
new Promise(...)
时,内部异步任务会立即执行。一旦调用resolve(value)
或者reject(error)
,Promise 的状态就固定了,并把状态缓存起来,存入内部的私有字段,多次调用resolve()
或reject()
也是无效的,会被忽略。 - 使用
.then
或.catch
只是拿到Promise
的结果,多次调用.then
或.catch
可以多次拿到结果,结果是相同的。 - 如果 Promise 通过
resolve(value)
缓存结果,调用.catch
是没有任何效果的,同样通过reject()
缓存结果,调用.then
也是没有任何效果的。
# 2 Promise执行多个任务
如果是多个任务呢,例如上面演示回调函数的三个任务:
function task1() {
return new Promise((resolve) => { // 省略了reject参数
setTimeout(() => {
console.log("任务1完成");
resolve("结果1");
}, 1000);
});
}
function task2(input) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("任务2,接收到任务1的数据:", input);
resolve("结果2");
}, 1000);
});
}
function task3(input) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("任务3,接收到任务2的数据:", input);
resolve("结果3");
}, 1000);
});
}
task1() // 第一个异步任务启动,1秒后输出“任务1完成”,返回 "结果1"
.then((result1) => {
// 进入这里,result1 是 "结果1"
return task2(result1); // 传入给 task2,继续异步调用,1秒后输出“任务2完成”
})
.then((result2) => {
// result2 是 "结果2"
return task3(result2); // 传入给 task3,继续异步调用,1秒后输出“任务3完成”
})
.then((result3) => {
// 最终收到 "结果3"
console.log("最终结果是:", result3);
})
.catch((err) => {
// 如果 task1 / task2 / task3 任何一个 reject,这里就会触发
console.error("出错了:", err);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- 首先每个任务使用 Promise 来管理执行;
- 一个任务执行完成,一定要使用return返回一个下一个新的 Promise,交给下一个
.then()
继续等待; - 如果过程中有任何一个任务调用
reject()
,则 catch 会被触发。
箭头函数可以简写,所以也可以简写如下:
task1()
.then(r1 => task2(r1))
.then(r2 => task3(r2))
.then(r3 => console.log("最终:", r3))
.catch(e => console.error("出错:", e));
2
3
4
5
# 19.4 Promise的常用方法
下面介绍一下 Promise 常用的几个静态的方法。
# 1 Promise.resolve()
Promise.resolve(value) 是 Promise 构造函数(类)提供的快捷方式,可以快速创建已完成的 Promise,返回一个状态为 fulfilled
(已完成)的 Promise,值为 value
。
举个栗子:
const p = Promise.resolve('Hello');
p.then(value => {
console.log('成功:', value); // 输出:成功:Hello
});
2
3
4
5
相当于:
const p = new Promise(resolve => resolve('Hello'));
p.then(value => {
console.log('成功:', value); // 输出:成功:Hello
});
2
3
4
5
# 2 Promise.reject()
const p = Promise.reject('出错了');
p.catch(reason => {
console.log('失败:', reason); // 输出:失败:出错了
});
2
3
4
5
相当于:
const p = new Promise((resolve, reject) => reject('出错了'));
p.catch(reason => {
console.log('失败:', reason); // 输出:失败:出错了
});
2
3
4
5
Promise.resolve()
和 Promise.reject()
可以快速创建 Promise 有一些场景下可能会用到,这里举几个栗子:
- 当你在
.catch()
中想继续流程,可以使用Promise.resolve(...)
返回默认值,保证后续.then()
还能继续链式调用:
fetch('/api/data')
.catch(err => {
console.error('请求失败,使用默认数据');
return Promise.resolve({ "data": [] });
})
.then(data => {
console.log('继续处理:', data);
});
2
3
4
5
6
7
8
fetch()
方法会发起 HTTP 请求,当网络请求完成,fetch()
的 Promise 被resolve
,可以理解为fetch
是一个 Promise 异步任务。
- 在某些条件不满足的地方,可以直接中断流程并抛出错误:
function checkPermission(user) {
if (!user.isAdmin) {
return Promise.reject('没有权限');
}
return fetch('/api/admin');
}
2
3
4
5
6
- 有时候我们写一个函数,不确定它是同步的还是异步的,但我们希望它始终返回 Promise,以方便统一调用:
function getUserInfo(cached = true) {
if (cached) {
// 本地缓存,立即返回结果
return Promise.resolve({ name: 'Tom', age: 18 });
} else {
// 异步获取
return fetch('/api/user').then(res => res.json());
}
}
getUserInfo(true).then(console.log);
getUserInfo(false).then(console.log);
2
3
4
5
6
7
8
9
10
11
12
# 3 Promise.all()
Promise.all()
可以并行执行多个 Promise,所有 Promise 成功才返回结果数组,有一个失败就立刻进入 .catch()
,短路。
举个栗子:
function fetchData(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`数据${id}`);
}, 1000 * id);
});
}
Promise.all([fetchData(1), fetchData(2), fetchData(3)])
.then((results) => {
console.log("所有数据:", results); // 所有数据:['数据1', '数据2', '数据3']
})
.catch((err) => {
console.error("有任务失败:", err);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在上面的代码中,使用
Promise.all()
并行执行三个 Promise,需要等到三个任务执行完成,才会返回结果; - 如果其中一个出错了(比如
reject()
),则直接进入catch
。
# 4 Promise.allSettled()
Promise.allSettled()
可以并行执行所有 Promise,不管成功或失败,全部执行完毕后才返回,和 Promise.all()
一样,不过每一项都有 { status, value | reason }
。
举个栗子:
const tasks = [
Promise.resolve("成功1"), // 创建一个立即完成的Promise
Promise.reject("失败2"), // 创建一个立即失败的Promise
Promise.resolve("成功3"),
];
Promise.allSettled(tasks).then((results) => {
console.log(results);
});
2
3
4
5
6
7
8
9
- 使用
Promise.allSettled()
同时执行三个 Promise,全部执行完毕才返回,并返回每一个任务的信息。 - 需要注意:
Promise.allSettled()
不会触发.catch()
,它永远都是.then()
执行结果如下:
[
{ status: 'fulfilled', value: '成功1' },
{ status: 'rejected', reason: '失败2' },
{ status: 'fulfilled', value: '成功3' }
]
2
3
4
5
# 5 Promise.race()
哪个先返回用哪个
Promise.race()
可以并行执行多个 Promise,谁先完成(成功或失败)就返回谁的结果,其他的忽略。
举个栗子:
const fast = new Promise((resolve) =>
setTimeout(() => resolve("快的"), 100)
);
const slow = new Promise((resolve) =>
setTimeout(() => resolve("慢的"), 1000)
);
Promise.race([fast, slow]).then((result) => {
console.log(result); // '快的'
});
2
3
4
5
6
7
8
9
10
- 上面两个异步任务,一个执行快一个执行慢,使用
Promise.race()
会得到先执行完成的任务结果。
# 6 Promise.any()
Promise.any()
可以并行执行多个 Promise,只要有一个成功就返回该成功值,所有都失败,才会进入 catch
。
举个例子:
const p1 = Promise.reject('失败1');
const p2 = Promise.resolve('成功2');
const p3 = Promise.resolve('成功3');
Promise.any([p1, p2, p3])
.then(result => {
console.log('成功:', result); // 输出 '成功2'
})
.catch(err => {
console.error('全失败', err);
});
2
3
4
5
6
7
8
9
10
11
- 上面创建了三个 Promise,同时执行三个任务,主要有一个成功,就返回。
# 19.5 async/await
async/await
是写法上的“语法糖”,可以以同步的方式书写异步代码,本质上依然是基于 Promise 实现的。
# 1 async/await的使用
举个栗子:
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
let data = {
name: 'for技术栈',
age: 18
}
resolve(JSON.stringify(data)); // 将数据返回
}, 1000);
});
}
async function main() {
console.log("请求开始");
const result = await fetchData();
console.log("收到结果:" + result);
}
main();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 在上面的代码中,fetchData 是异步的方法,在 main 方法中,可以通过
await fetchData()
调用异步方法,待会会等待 Promise resolve 后才会继续执行。main 方法需要使用async
来修饰。 - 需要注意
await
只能在 async 函数中使用。 - 那么上面调用异步的方法就可以变成同步的方式了。
执行结果:
请求开始
收到结果:{"name":"for技术栈","age":18}
2
所以上面使用 Promise 链式调用三个任务的代码:
task1()
.then(r1 => task2(r1))
.then(r2 => task3(r2))
.then(r3 => console.log("最终:", r3))
.catch(e => console.error("出错:", e));
2
3
4
5
就可以变成如下的方式:
async function runTasks() {
try {
const result1 = await task1();
const result2 = await task2(result1);
const result3 = await task3(result2);
console.log("最终结果:", result3);
} catch (err) {
console.error("出错:", err);
}
}
runTasks();
2
3
4
5
6
7
8
9
10
11
12
# 2 阻塞
先看一下代码:
async function fetchData() {
console.log("开始请求");
const response = await fetch("https://www.foooor.com/apis/"); // 默认发起get请求
const data = await response.json();
console.log("数据获取完成", data);
}
console.log("准备获取数据");
fetchData();
console.log("我不等你,先打印这句");
2
3
4
5
6
7
8
9
10
在上面的代码中,
fetch()
开始执行,会发起 HTTP 请求,当网络请求完成,fetch()
的 Promise 被resolve
,会继续执行await
之后的逻辑。需要注意,
await
表示等待一个异步操作完成,但是它只会阻塞当前async function
函数内的代码,不会阻塞主线程或其他任务。
所以上面的代码,执行结果如下:
准备获取数据
开始请求
我不等你,先打印这句
数据获取完成 {msg: 'Hello For技术栈', code: 200}
2
3
4
# 3 async函数的返回值
所有 async
函数都会自动返回一个 Promise:
async function test() {
return 123;
}
test().then(res => console.log(res)); // 输出:123
2
3
4
5
- 即便你 return 的是普通值,它也会被包装成
Promise.resolve(123)
。 Promise.resolve(123)
是创建一个立即完成的Promise
。
# 19.5 事件循环
前面在讲解定时器的时候,已经讲解过事件循环和任务队列。
但其实 JavaScript 有两类任务队列:宏任务队列、微任务队列。
setTimeout
、setInterval
、I/O
、setImmediate
, MessageChannel
等宏任务(Macro Task)会被放入到宏任务队列,Promise.then()
、Promise.catch()
、async/await
(await 后的逻辑)queueMicrotask
, MutationObserver
等微任务(Micro Task)会被放入到微任务队列。
那么执行的规则是什么样的呢?
举个栗子:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => { // Promise.resolve()是创建一个立即完成的 Promise
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
2
3
4
5
6
7
8
9
10
11
12
13
- 上面的代码在主线程中执行,有同步代码、定时器、Promise。
它们会按照如下的规则执行:
执行主线程同步代码
- 所有 同步代码 立即进入 调用栈 执行;
- 期间遇到异步任务,会加入对应队列(微任务队列/宏任务队列)。
清空微任务队列
- 一旦主线程执行完(即调用栈为空),开始执行所有微任务;
- 微任务也是一个个压入调用栈执行;
- 微任务执行过程中如果又注册了新的微任务,也会立即加入微任务队列,然后依次压入调用栈;
- 直到微任务队列彻底清空为止。
执行一个宏任务
- 微任务清空后,从 宏任务队列中取出一个宏任务,将其压入调用栈执行;
- 执行期间又可能产生微任务或新的宏任务,分别加入到对应的任务队列,等待执行,
- 注意,这里只执行一个宏任务;
进入下一轮事件循环(Event Loop)
- 再次清空微任务队列。
- 然后再执行下一个宏任务。
- 如此循环往复。
所以上面的代码的执行结果如下:
script start
script end
promise1
promise2
setTimeout
2
3
4
5
# 19.6 错误处理
需要注意,Promise 错误不会被 try/catch 捕获。
举个栗子:
try {
Promise.reject('出错了');
} catch (e) {
console.log('永远不会执行'); // 不会捕获
}
2
3
4
5
正确写法:
Promise.reject('出错了')
.catch((e) => console.error(e));
2
或者 async/await:
async function test() {
try {
await Promise.reject('出错了');
} catch (e) {
console.error('捕获错误:', e);
}
}
2
3
4
5
6
7