# JavaScript教程 - 19 异步编程

# 19.1 异步和同步

什么是同步和异步?

同步就是代码一行一行执行,前一行不执行完,后一行不会执行。

但有时候某些操作(如网络请求、文件读取、定时器等)可能很慢,这时候如果继续同步执行,会“阻塞”后续代码执行,影响用户体验。

异步编程的出现就是为了解决这个问题:让程序能在等待某些操作完成的同时,继续做别的事。

举个栗子:

同步代码:

console.log('1');
console.log('2');
console.log('3');

// 输出顺序就是 1 -> 2 -> 3
1
2
3
4
5
  • 上面的代码,一行一行按照顺序执行。

异步代码:

console.log('1');
setTimeout(() => {
  console.log('2');
}, 1000);
console.log('3');

// 实际输出顺序是:1 -> 3 -> (等待1秒)-> 2
1
2
3
4
5
6
7
  • setTimeout 是一种异步方法,它不会阻塞主线程。

# 19.2 回调地狱

异步最常见的方式就是回调函数,举个栗子:

function doSomethingAsync(callback) {
  setTimeout(() => {
    console.log('执行任务...');
    callback(); // 任务完成后再执行回调
  }, 1000);
}

doSomethingAsync(() => {
  console.log('回调被调用');
});
1
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);
    });
  });
});
1
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);
});
1
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);
  });
1
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);
  });
1
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));
1
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
});
1
2
3
4
5

相当于:

const p = new Promise(resolve => resolve('Hello'));

p.then(value => {
  console.log('成功:', value); // 输出:成功:Hello
});
1
2
3
4
5

# 2 Promise.reject()

const p = Promise.reject('出错了');

p.catch(reason => {
  console.log('失败:', reason); // 输出:失败:出错了
});
1
2
3
4
5

相当于:

const p = new Promise((resolve, reject) => reject('出错了'));

p.catch(reason => {
  console.log('失败:', reason); // 输出:失败:出错了
});
1
2
3
4
5

Promise.resolve()Promise.reject() 可以快速创建 Promise 有一些场景下可能会用到,这里举几个栗子:

  1. 当你在 .catch() 中想继续流程,可以使用 Promise.resolve(...) 返回默认值,保证后续 .then() 还能继续链式调用:
fetch('/api/data')
  .catch(err => {
    console.error('请求失败,使用默认数据');
    return Promise.resolve({ "data": [] });
  })
  .then(data => {
    console.log('继续处理:', data);
  });
1
2
3
4
5
6
7
8
  • fetch() 方法会发起 HTTP 请求,当网络请求完成,fetch() 的 Promise 被 resolve ,可以理解为 fetch 是一个 Promise 异步任务。
  1. 在某些条件不满足的地方,可以直接中断流程并抛出错误:
function checkPermission(user) {
  if (!user.isAdmin) {
    return Promise.reject('没有权限');
  }
  return fetch('/api/admin');
}
1
2
3
4
5
6
  1. 有时候我们写一个函数,不确定它是同步的还是异步的,但我们希望它始终返回 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);
1
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);
  });
1
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);
});
1
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' }
]
1
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); // '快的'
});
1
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);
  });
1
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();
1
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}
1
2

所以上面使用 Promise 链式调用三个任务的代码:

task1()
  .then(r1 => task2(r1))
  .then(r2 => task3(r2))
  .then(r3 => console.log("最终:", r3))
  .catch(e => console.error("出错:", e));
1
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();
1
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("我不等你,先打印这句");
1
2
3
4
5
6
7
8
9
10
  • 在上面的代码中,fetch() 开始执行,会发起 HTTP 请求,当网络请求完成,fetch() 的 Promise 被 resolve,会继续执行 await 之后的逻辑。

  • 需要注意,await 表示等待一个异步操作完成,但是它只会阻塞当前 async function 函数内的代码,不会阻塞主线程或其他任务。

所以上面的代码,执行结果如下:

准备获取数据
开始请求
我不等你,先打印这句
数据获取完成 {msg: 'Hello For技术栈', code: 200}
1
2
3
4

# 3 async函数的返回值

所有 async 函数都会自动返回一个 Promise:

async function test() {
  return 123;
}

test().then(res => console.log(res)); // 输出:123
1
2
3
4
5
  • 即便你 return 的是普通值,它也会被包装成 Promise.resolve(123)
  • Promise.resolve(123) 是创建一个立即完成的 Promise

# 19.5 事件循环

前面在讲解定时器的时候,已经讲解过事件循环和任务队列。

但其实 JavaScript 有两类任务队列:宏任务队列、微任务队列。

setTimeoutsetIntervalI/OsetImmediate, 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');
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 上面的代码在主线程中执行,有同步代码、定时器、Promise。

它们会按照如下的规则执行:

  1. 执行主线程同步代码

    • 所有 同步代码 立即进入 调用栈 执行;
    • 期间遇到异步任务,会加入对应队列(微任务队列/宏任务队列)。
  2. 清空微任务队列

    • 一旦主线程执行完(即调用栈为空),开始执行所有微任务
    • 微任务也是一个个压入调用栈执行
    • 微任务执行过程中如果又注册了新的微任务,也会立即加入微任务队列,然后依次压入调用栈;
    • 直到微任务队列彻底清空为止
  3. 执行一个宏任务

    • 微任务清空后,从 宏任务队列中取出一个宏任务,将其压入调用栈执行;
    • 执行期间又可能产生微任务或新的宏任务,分别加入到对应的任务队列,等待执行,
    • 注意,这里只执行一个宏任务
  4. 进入下一轮事件循环(Event Loop)

    • 再次清空微任务队列。
    • 然后再执行下一个宏任务。
    • 如此循环往复。

所以上面的代码的执行结果如下:

script start
script end
promise1
promise2
setTimeout
1
2
3
4
5

# 19.6 错误处理

需要注意,Promise 错误不会被 try/catch 捕获。

举个栗子:

try {
  Promise.reject('出错了');
} catch (e) {
  console.log('永远不会执行'); // 不会捕获
}
1
2
3
4
5

正确写法:

Promise.reject('出错了')
  .catch((e) => console.error(e));
1
2

或者 async/await:

async function test() {
  try {
    await Promise.reject('出错了');
  } catch (e) {
    console.error('捕获错误:', e);
  }
}
1
2
3
4
5
6
7