异步编程
JavaScript 是单线程的,意味着它一次只能执行一个任务。如果遇到耗时操作(如网络请求),程序会“阻塞”,导致页面卡顿。为了解决这个问题,JavaScript 使用异步编程,让耗时操作在后台执行,同时继续执行其他代码。
异步的特点:
- 不会阻塞后续代码的执行。
- 耗时操作完成后,通过回调函数、Promise 或 async/await 来处理结果。
也就是实现异步编程有 3 种方法:回调函数、Promise 或 async/await
回调函数容易出现”回调地狱”的情况,所以目前不怎么依赖它来实现异步编程,我们简单介绍即可
回调函数
回调函数本质就是一个函数,只是把它作为参数传递给了另一个函数,并在某个操作完成后被调用。回调函数的核心思想是:
- 延迟执行:在某个条件满足或某个操作完成后,才执行这个函数。
- 异步处理:常用于处理异步操作的结果。
示例:
1 | // 1. 定义一个回调函数(取餐后的动作) |
输出结果:
1 | 点餐:汉堡 |
Promise
本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了,它表示一个异步操作的最终完成(或失败)及其结果值。你可以把它理解为一个“承诺”:
- 承诺:将来会完成某个操作(成功或失败)
- 结果:操作完成后,会返回一个值(或错误)
Promise 的三种状态
一个 Promise 对象有三种状态:
- Pending(进行中):初始状态,表示操作尚未完成。
- Fulfilled(已成功):表示操作成功完成,并返回结果值。
- Rejected(已失败):表示操作失败,并返回错误原因。
一旦 Promise 的状态从 Pending 变为 Fulfilled 或 Rejected,就不可再改变。
then()
.then()
是 Promise 对象的一个方法,用于处理 Promise 成功完成后的结果。它是 JavaScript 中实现异步编程的核心工具之一,让代码更清晰、更易读
.then()
的作用
- 处理成功的结果:当 Promise 的状态从 Pending 变为 Fulfilled(即成功)时,
.then()
中的回调函数会被调用。 - 链式调用:
.then()
返回一个新的 Promise,可以继续调用下一个.then()
,从而实现多个异步操作的顺序执行。
.then()的语法
1 | promise.then(onFulfilled, onRejected); |
- **
onFulfilled
**:一个回调函数,当 Promise 成功时调用。它接受一个参数,即 Promise 的成功结果。 - **
onRejected
**(可选):一个回调函数,当 Promise 失败时调用。它接受一个参数,即 Promise 的失败原因。
通常,我们只使用 onFulfilled
,而用 .catch()
来处理失败的情况。
链式调用-示例:
1 | function doSomething() { |
输出结果:
1 | 得到最终结果:第一步结果 -> 第二步结果 -> 第三步结果 |
async/
await
async/await 是 JavaScript 中用于简化异步编程的语法糖。它基于 Promise,但让异步代码的写法更像同步代码,从而更容易理解和维护。
async:用于声明一个异步函数。异步函数会隐式返回一个 Promise
await:用于等待一个 Promise 完成(即 Promise 的状态变为 resolved)。await 只能在 async 函数中使用
promise章节中的代码用async/
await简写如下:
1 | async function main() { |
💡关于语法糖
不改变语言的功能:语法糖只是提供了一种更简洁的写法,底层实现仍然是原有的语法或机制。提高代码的可读性:语法糖通常会让代码更直观、更易理解。
减少代码量:语法糖可以帮助开发者用更少的代码实现相同的功能。
闭包
闭包是指一个函数能够“记住”并访问它定义时的环境,即使这个函数在定义时的环境之外执行。即 闭包 = 函数 + 引用环境
闭包的核心
函数嵌套:
- 闭包通常发生在函数嵌套的情况下,即一个函数内部定义了另一个函数。
访问外部变量:
- 内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。
“记住”环境:
- 闭包会“记住”它定义时的环境(即外部函数的作用域),即使外部函数已经执行完毕。
示例 1:简单的闭包
1 | function outer() { |
outer
函数内部有一个变量message
。inner
函数使用了message
。- 即使
outer
函数执行完毕,inner
函数仍然可以访问message
,这就是闭包。
闭包的作用
记住变量:
- 闭包可以让函数“记住”它定义时的变量,即使外部函数已经执行完毕。
创建私有变量:
- 闭包可以用来隐藏变量,避免被外部直接修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function createPerson(name) {
let age = 0; // 私有变量
return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
celebrateBirthday: function() {
age++;
}
};
}
const person = createPerson("Alice");
console.log(person.getName()); // 输出: Alice
person.celebrateBirthday();
console.log(person.getAge()); // 输出: 1实现模块化:
- 闭包可以用来隐藏内部实现细节,只暴露必要的功能。
1
2
3
4
5
6
7
8
9
10
11const module = (function() {
const privateVariable = "I am private";
return {
publicMethod: function() {
console.log(privateVariable);
}
};
})();
module.publicMethod(); // 输出: I am private
闭包的注意事项
- 内存泄漏:
- 闭包会一直“记住”外部函数的变量,即使这些变量已经不再需要了。如果闭包用得太多,可能会导致内存占用过高。
- 解决方法:在不需要时手动解除引用。
1 | function outer() { |
🖥️ JavaScript的单线程和异步并发原理
1. JavaScript是单线程的
- 只有一个执行线程(主线程)执行代码,任何时刻只能执行一个任务。
- 这就意味着,代码是逐行执行,同步任务一个接一个。
2. 为什么需要异步?
- 有些操作耗时,比如网络请求、文件读取、定时器。
- 如果这些操作都是同步执行,JS主线程会被堵塞,页面卡死。
- 所以引入异步机制,避免阻塞,提升用户体验。
3. 事件循环(Event Loop)机制
核心就是:
- 主线程有一个任务栈(Call Stack),同步任务进栈执行,执行完出栈。
- 异步任务(比如setTimeout、Promise等)会被放入任务队列(Task Queue / Microtask Queue)。
- 事件循环不断检查任务栈是否为空,空了就去任务队列里取任务执行。
4. 任务队列分两种
- 宏任务(Macrotasks):setTimeout、setInterval、I/O、UI渲染等。
- 微任务(Microtasks):Promise的.then/catch/finally,MutationObserver等。
事件循环的规则:
- 执行主线程的同步代码(任务栈)。
- 主线程空了,执行所有微任务队列里的任务。
- 微任务清空后,执行一个宏任务。
- 重复以上步骤。
5. 并发效果但不是并行
虽然只有一个线程,异步任务通过事件循环机制,实现了逻辑上的并发:
- 你发起多个异步请求,浏览器会帮你管理它们。
- JS主线程不等待结果,继续执行其他代码。
- 异步操作完成后,回调函数排入任务队列,等待主线程空闲时执行。
所以,看起来是“多个任务同时进行”,其实是“任务轮流执行”,这就是并发,而不是硬件层面的并行。
6. 举例说明
1 | console.log('start'); |
执行顺序是:
1 | start |
console.log('start')
同步执行。setTimeout
的回调是宏任务,先放到宏任务队列。Promise.then
的回调是微任务,放到微任务队列。console.log('end')
同步执行。- 主线程空了,先执行所有微任务(打印 ‘promise’)。
- 再执行宏任务(打印 ‘timeout’)。
7. 总结
术语 | JS中体现 |
---|---|
单线程 | JS只有一个主执行线程 |
并发 | 异步任务通过事件循环交替执行 |
并行 | 需要多核CPU或Web Worker支持 |
事件循环 | 管理同步/异步任务的执行顺序 |
微任务/宏任务队列 | 按优先级执行异步回调 |
进阶内容之 Web Worker —— JavaScript的多线程实现
1. 什么是 Web Worker?
- Web Worker 是浏览器提供的 后台线程,可以在主线程之外运行脚本。
- 这样,耗时的计算或任务不会阻塞页面主线程,提升性能和用户体验。
- 主线程和 Worker 线程是 独立的线程,它们之间通过消息传递通信。
作用
- Web Worker 是浏览器给JavaScript的“多线程”支持,帮助实现并行。
- 主线程和 Worker 线程相互独立,通过消息传递交流。
- 解决了 JS 单线程阻塞的问题,提升性能和响应速度。
2. 为什么需要 Web Worker?
- JavaScript 本身是单线程,耗时任务(大计算、复杂数据处理)会阻塞UI,导致页面卡顿。
- 使用 Web Worker,可以把耗时任务放到后台线程,主线程继续响应用户操作。
3. Web Worker 的基本用法
主线程代码(main.js)
1 | jsCopy code// 创建一个Worker,指定运行的脚本文件worker.js |
Worker线程代码(worker.js)
1 | jsCopy code// 监听主线程发送的消息 |
4. Web Worker 的特点
- 运行在独立线程,拥有自己的运行环境。
- 不能访问 DOM 和全局变量,只能通过消息通信。
- 只能用有限的API(比如不能直接操作window、document)。
- 适合做大量计算、数据处理、图像处理等。
5. Web Worker vs 主线程
比较项 | 主线程 | Web Worker |
---|---|---|
线程数 | 单线程 | 独立多线程 |
DOM访问 | 可以直接访问DOM | 不能访问DOM,只能通信 |
阻塞风险 | 大量计算会阻塞UI | 不阻塞UI,后台运行 |
通信方式 | 直接操作变量/函数 | 通过 postMessage 传递消息 |
使用场景 | UI渲染、事件响应 | 复杂计算、数据处理 |