什么是异步编程
所谓异步,就是在做一件耗时的事时,不必卡在原地干等,而是可以先去忙别的,等结果出来了再通过回调、事件之类的机制处理它。
举个栗子:你在网上订了张火车票。提交订单后,不用一直盯着那个处理页面,可以去收拾行李、规划行程。等订票成功或者出问题了,系统会通过短信或 App 推送通知你结果。这个订票过程就是异步的——你不需要等结果出来,才能做别的事。
为什么JS需要异步编程
JS 最初是为浏览器设计的,属单线程,同一时刻只能执行一项任务。遇到网络请求、文件读写这类耗时操作,程序会一直卡到它完成。
为了不阻塞,JS 引入了事件循环机制来实现非阻塞操作,这也是异步编程的核心。
JS 需要异步编程,主要有这几点:
- 提升用户体验。页面上一旦跑同步的耗时任务,就会卡顿甚至无响应,用户多半直接关掉。异步能让页面在任务进行时照常响应操作。比如点击按钮触发数据请求,同步会卡住,异步则能继续其他操作。
- 充分利用资源。浏览器和服务器资源都有限,任务一阻塞就是浪费。异步可以在等待的间隙处理别的任务。比如电商网站把商品加进购物车时,异步更新库存不会打断用户浏览。
- 处理网络请求。现代 Web 应用里网络请求很常见,耗时且不稳定,异步能更好应对,不至于让程序一直等响应。比如社交网站加载好友动态,异步能让用户同时做其他操作。
- 适应多任务环境。应用常常要同时处理多个任务,异步让 JS 能应付得来。比如在线协作工具里多个用户同时编辑文档,异步能及时处理各自的操作,互不阻塞。
浏览器的事件循环
浏览器的事件循环,就是一套管理和协调 JavaScript 代码执行、处理各种异步任务的机制。
它的工作原理大致如下:
首先,JavaScript 运行在一个单线程环境里,称为主线程。同步代码按顺序依次执行。遇到异步任务,比如网络请求、定时器、用户交互事件(点击、滚动等),这些任务会被放进浏览器的任务队列。
主线程会一直执行同步任务,直到全部执行完,再去检查任务队列里有没有可以执行的异步回调。有就取出来执行。任务队列通常分为宏任务队列和微任务队列。
微任务-Micro-tasks
- Promise 的
.then()、.catch()和.finally()回调 - async / await 语法中的 await 关键字后面的代码
- MutationObserver 监听 DOM 变化时的回调函数
宏任务-Macro-tasks
- 定时器事件:
setTimeout、setInterval - 渲染事件:解析 DOM、计算布局、绘制
- 交互事件:如鼠标点击、键盘事件
- I/O 操作:网络请求、文件读写
在每次执行完一个宏任务后,会先清空微任务队列中的所有微任务,然后再去检查宏任务队列。
例如,当你在网页里点击一个按钮触发异步操作,这个操作会被放进任务队列,主线程空闲时再处理它的回调。再比如使用 Promise 时,then 里的回调会被放进微任务队列,确保当前宏任务执行完就尽快执行。
演示代码
使用 setTimeout
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
在这个例子中,输出顺序为 1、3、2。尽管 setTimeout 的时间设置为 0 毫秒,但它仍然会在所有同步任务完成后才执行。
结合 Promise
console.log("1");
Promise.resolve().then(() => {
console.log("2");
});
setTimeout(() => {
console.log("3");
}, 0);
console.log("4");
这里的输出顺序是:1、4、2、3。因为在每个宏任务执行完后,会先处理微任务队列中的任务。
多个 setTimeout 和 Promise
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
setTimeout(() => {
console.log("4");
}, 0);
console.log("5");
Promise.resolve().then(() => {
console.log("6");
});
输出顺序为:1、5、3、6、2、4。
总结一下:
Node.js的事件循环
Node.js 的事件循环和浏览器里的有相似之处,但也有些差异。
Node.js 的事件循环基于 libuv 库实现,它主要有 6 个阶段:
- timers:处理通过 setTimeout() 和 setInterval() 设定的到期定时器回调。
- pending callbacks:执行一些系统操作的回调,例如 TCP 错误。
- idle, prepare:内部使用,普通用户代码通常不涉及。
- poll:获取新的 I/O 事件,例如网络请求、文件读取等。在这个阶段,如果没有 I/O 事件,会在一定条件下阻塞。
- check:执行 setImmediate() 的回调。
- close callbacks:处理一些关闭的回调,例如 socket 关闭。
比如在 Node.js 里写 setTimeout(() => { console.log('Timeout!'); }, 500),500 毫秒一到,回调就在 timers 阶段被处理。
再比如 setImmediate(() => { console.log('Immediate!'); }),回调会在 check 阶段被处理。
在事件循环的每次迭代中,会依次检查这些阶段,按照顺序执行相应的任务和回调。
宏任务与微任务
跟浏览器中的js一样,node中的异步代码也分为宏任务和微任务,只是它们之间的执行顺序有所区别。 我们再来看看Node中都有哪些宏任务和微任务
宏任务
微任务
对于微任务我们还有个点需要特别注意。那就是虽然 nextTick 同属于微任务,但是它的优先级是高于其它微任务,在执行微任务时,只有nextlick中的所有回调函数执行完成后才会开始执行其它微任务。
演示代码
setTimeout 和 setImmediate 的执行顺序
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
setImmediate(() => {
console.log("3");
});
console.log("4");
在这个例子中,setTimeout 和 setImmediate 的执行顺序不一定,有时 setTimeout 回调先执行,有时 setImmediate 回调先执行,这取决于当时的系统状态。
结合 Promise 和 process.nextTick
console.log("1");
process.nextTick(() => {
console.log("2");
});
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
输出顺序为:1、4、2、3。首先,执行同步输出1、4。接着,process.nextTick 的回调会在当前操作结束后立即执行,所以输出 2。
最后,Promise.resolve().then 属于微任务,在当前宏任务(这里指同步代码的执行)结束后执行,所以输出 3。
多个阶段的综合示例
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
setImmediate(() => {
console.log("3");
});
process.nextTick(() => {
console.log("4");
});
Promise.resolve().then(() => {
console.log("5");
});
console.log("6");
输出顺序为:1、6、4、5、3、2。首先,执行同步代码输出 1、6。然后,process.nextTick 的回调会在当前操作结束后立即执行,所以输出 4。 接着,Promise.resolve().then 的回调作为微任务被执行,输出 5。再之后,进入事件循环的检查阶段,执行 setImmediate 的回调,输出 3。 最后,当满足 setTimeout 的时间条件时,执行其回调,输出 2。
Promise
Promise 是 JavaScript 里处理异步操作的一种对象。
它代表一个可能在未来完成或失败的操作,用更结构化、也更好读的方式来表达异步结果。
Promise 有三种状态:Pending(进行中)、Fulfilled(已成功)和 Rejected(已失败)。
它有几个特点:
- 不可撤销:耗时较长的异步操作,一旦开始就没法中途停下。
- 错误处理:如果没设回调,Promise 内部抛出的错误在外部是看不到的。
- 单次处理特性:Promise 仅能实现一次解决或拒绝,它不能应对重复出现的事件。
当创建一个 Promise 时,它初始处于 Pending 状态。 异步操作完成后,如果成功,Promise 会转变为 Fulfilled 状态,并携带一个结果值; 如果失败,Promise 会转变为 Rejected 状态,并携带一个错误对象。
通过 then 方法可以指定 Promise 对象成功和失败时的回调函数,分别处理操作成功的结果和失败的原因。例如:
const myPromise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
if (Math.random() > 0.5) {
resolve("操作成功!");
} else {
reject("操作失败!");
}
}, 1000);
});
myPromise.then(
(result) => {
console.log(result);
},
(error) => {
console.error(error);
},
);
Promise 让异步代码更易于理解、组合和管理,也避免了回调地狱。
链式调用
function task1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Task 1 completed");
resolve("Result from Task 1");
}, 1000);
});
}
function task2(resultFromTask1) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Task 2 received: ${resultFromTask1}`);
resolve("Result from Task 2");
}, 1500);
});
}
function task3(resultFromTask2) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Task 3 received: ${resultFromTask2}`);
resolve("Final result");
}, 2000);
});
}
task1()
.then((result) => task2(result))
.then((result) => task3(result))
.then((result) => console.log(result))
.catch((error) => console.error(error));
在面的代码中,每个任务都返回一个 Promise ,然后通过 then 依次串联起来,形成一条异步操作链。中间某个环节出错,都能被最后的 catch 统一处理。
.then() 方法最多接受两个参数;第一个参数是 Promise 兑现时的回调函数,第二个参数是 Promise 拒绝时的回调函数。
每个 .then() 返回一个新生成的 Promise 对象,这个对象可被用于链式调用,例如:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("omom");
}, 300);
});
myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
链式调用怎么实现的?
Promise 的链式调用是通过 then 方法返回新的 Promise 对象来实现的。
当一个 Promise 通过 then 方法注册成功回调或失败回调时,then 方法会返回一个新的 Promise 对象。
如果 then 方法中的回调函数返回一个值(不是 Promise 对象),那么新返回的 Promise 就会以这个值为成功的结果进入 Fulfilled 状态。
如果回调函数抛出一个错误,或者返回一个被拒绝的 Promise 对象,那么新返回的 Promise 就会以这个错误或拒绝的状态进入 Rejected 状态。
const promise1 = new Promise((resolve, reject) => {
resolve("First promise resolved");
});
const promise2 = promise1.then((result) => {
// 这里返回一个值,新的 promise2 以这个值进入成功状态
return "Second result";
});
const promise3 = promise2.then((result) => {
// 这里可以继续基于上一个 promise 的结果进行处理和返回新的值
if (result === "Second result") {
return "Third result";
} else {
throw new Error("Wrong result");
}
});
通过这种方式,一个 Promise 的结果可以作为下一个 Promise 的输入,从而实现了链式调用,使得异步操作可以以一种更清晰、更可维护的方式串联起来。
Async / Await
async/await 是 JavaScript 里处理异步操作的一种更简洁、更直观的语法。
async 用于定义一个异步函数。异步函数总是返回一个 Promise 对象。 函数内部返回一个值,async 会自动把它包装成已解决(fulfilled)的 Promise。 函数内部抛出错误,async 会自动把它包装成被拒绝(rejected)的 Promise。
await 只能在 async 函数内部使用,用来暂停异步函数的执行,等待一个 Promise 解决并拿到它的值。如果等待的 Promise 被拒绝,await 会把错误抛出来。
async function getData() {
try {
const response = await fetch("https://example.com/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching data:", error);
}
}
getData().then((result) => console.log(result));
async/await 让异步代码看起来更像同步代码,读起来更顺,也避开了回调地狱和层层嵌套的 Promise 链。