0%

Javascript-异步

异步编程

JavaScript 是单线程的,意味着它一次只能执行一个任务。如果遇到耗时操作(如网络请求),程序会“阻塞”,导致页面卡顿。为了解决这个问题,JavaScript 使用异步编程,让耗时操作在后台执行,同时继续执行其他代码。

异步的特点:

  • 不会阻塞后续代码的执行。
  • 耗时操作完成后,通过回调函数、Promise 或 async/await 来处理结果。

也就是实现异步编程有 3 种方法:回调函数、Promise 或 async/await

回调函数容易出现”回调地狱”的情况,所以目前不怎么依赖它来实现异步编程,我们简单介绍即可


回调函数

回调函数本质就是一个函数,只是把它作为参数传递给了另一个函数,并在某个操作完成后被调用。回调函数的核心思想是:

  • 延迟执行:在某个条件满足或某个操作完成后,才执行这个函数。
  • 异步处理:常用于处理异步操作的结果。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 定义一个回调函数(取餐后的动作)
function takeFood(food) {
console.log("取餐:" + food);
}

// 2. 定义一个函数(点餐)
function orderFood(callback) {
console.log("点餐:汉堡");
setTimeout(() => {
const food = "汉堡";
callback(food); // 3秒后调用回调函数
}, 3000);
}

// 3. 点餐,并告诉服务员取餐时调用 takeFood 函数
orderFood(takeFood);

// 4. 继续做其他事情
console.log("等待取餐中,先玩手机...");

输出结果:

1
2
3
点餐:汉堡
等待取餐中,先玩手机...
取餐:汉堡

Promise

本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了,它表示一个异步操作的最终完成(或失败)及其结果值。你可以把它理解为一个“承诺”:

  • 承诺:将来会完成某个操作(成功或失败)
  • 结果:操作完成后,会返回一个值(或错误)

Promise 的三种状态

一个 Promise 对象有三种状态:

  1. Pending(进行中):初始状态,表示操作尚未完成。
  2. Fulfilled(已成功):表示操作成功完成,并返回结果值。
  3. 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
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
function doSomething() {
return new Promise((resolve) => {
setTimeout(() => resolve("第一步结果"), 1000);
});
}

function doSomethingElse(result) {
return new Promise((resolve) => {
setTimeout(() => resolve(result + " -> 第二步结果"), 1000);
});
}

function doThirdThing(result) {
return new Promise((resolve) => {
setTimeout(() => resolve(result + " -> 第三步结果"), 1000);
});
}

function failureCallback(error) {
console.error("出错:", error);
}

doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`得到最终结果:${finalResult}`);
})
.catch(failureCallback);

输出结果:

1
得到最终结果:第一步结果 -> 第二步结果 -> 第三步结果

async/await

async/await 是 JavaScript 中用于简化异步编程的语法糖。它基于 Promise,但让异步代码的写法更像同步代码,从而更容易理解和维护。

async:用于声明一个异步函数。异步函数会隐式返回一个 Promise

await:用于等待一个 Promise 完成(即 Promise 的状态变为 resolved)。await 只能在 async 函数中使用

promise章节中的代码用async/await简写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
async function main() {
try {
const result1 = await doSomething(); // 等待第一步完成
const result2 = await doSomethingElse(result1); // 等待第二步完成
const finalResult = await doThirdThing(result2); // 等待第三步完成
console.log(`得到最终结果:${finalResult}`);
} catch (error) {
failureCallback(error); // 捕获错误
}
}

// 调用 main 函数
main();

💡关于语法糖
不改变语言的功能:语法糖只是提供了一种更简洁的写法,底层实现仍然是原有的语法或机制。

提高代码的可读性:语法糖通常会让代码更直观、更易理解。

减少代码量:语法糖可以帮助开发者用更少的代码实现相同的功能。


闭包

闭包是指一个函数能够“记住”并访问它定义时的环境,即使这个函数在定义时的环境之外执行。即 闭包 = 函数 + 引用环境

闭包的核心

  1. 函数嵌套

    • 闭包通常发生在函数嵌套的情况下,即一个函数内部定义了另一个函数。
  2. 访问外部变量

    • 内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。
  3. “记住”环境

    • 闭包会“记住”它定义时的环境(即外部函数的作用域),即使外部函数已经执行完毕。

示例 1:简单的闭包

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
const message = "Hello, Closure!"; // 外部函数的变量

function inner() {
console.log(message); // 内部函数访问外部函数的变量
}

return inner; // 返回内部函数
}

const innerFunc = outer(); // outer 执行完毕
innerFunc(); // 输出: Hello, Closure!
  • outer 函数内部有一个变量 message
  • inner 函数使用了 message
  • 即使 outer 函数执行完毕,inner 函数仍然可以访问 message,这就是闭包。

闭包的作用

  1. 记住变量

    • 闭包可以让函数“记住”它定义时的变量,即使外部函数已经执行完毕。
  2. 创建私有变量

    • 闭包可以用来隐藏变量,避免被外部直接修改。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function 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
  3. 实现模块化

    • 闭包可以用来隐藏内部实现细节,只暴露必要的功能。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const module = (function() {
    const privateVariable = "I am private";

    return {
    publicMethod: function() {
    console.log(privateVariable);
    }
    };
    })();

    module.publicMethod(); // 输出: I am private

闭包的注意事项

  1. 内存泄漏
    • 闭包会一直“记住”外部函数的变量,即使这些变量已经不再需要了。如果闭包用得太多,可能会导致内存占用过高。
    • 解决方法:在不需要时手动解除引用。
1
2
3
4
5
6
7
8
9
10
11
function outer() {
const largeData = new Array(1000000).fill("data");

return function() {
console.log("Closure!");
};
}

const innerFunc = outer();
// 不再需要时解除引用
innerFunc = null;

🖥️ 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等。

事件循环的规则:

  1. 执行主线程的同步代码(任务栈)。
  2. 主线程空了,执行所有微任务队列里的任务。
  3. 微任务清空后,执行一个宏任务。
  4. 重复以上步骤。

5. 并发效果但不是并行

虽然只有一个线程,异步任务通过事件循环机制,实现了逻辑上的并发

  • 你发起多个异步请求,浏览器会帮你管理它们。
  • JS主线程不等待结果,继续执行其他代码。
  • 异步操作完成后,回调函数排入任务队列,等待主线程空闲时执行。

所以,看起来是“多个任务同时进行”,其实是“任务轮流执行”,这就是并发,而不是硬件层面的并行


6. 举例说明

1
2
3
4
5
6
7
8
9
10
11
console.log('start');

setTimeout(() => {
console.log('timeout');
}, 0);

Promise.resolve().then(() => {
console.log('promise');
});

console.log('end');

执行顺序是:

1
2
3
4
start
end
promise
timeout
  • 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
2
3
4
5
6
7
8
9
10
jsCopy code// 创建一个Worker,指定运行的脚本文件worker.js
const worker = new Worker('worker.js');

// 监听Worker发送来的消息
worker.onmessage = function(event) {
console.log('从worker收到消息:', event.data);
};

// 向Worker发送消息
worker.postMessage('开始计算');

Worker线程代码(worker.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
jsCopy code// 监听主线程发送的消息
onmessage = function(event) {
console.log('Worker收到消息:', event.data);

// 执行一些耗时计算,比如大循环
let count = 0;
for (let i = 0; i < 1e9; i++) {
count += i;
}

// 计算完,向主线程发送结果
postMessage('计算完成,结果是:' + count);
};

4. Web Worker 的特点

  • 运行在独立线程,拥有自己的运行环境。
  • 不能访问 DOM 和全局变量,只能通过消息通信。
  • 只能用有限的API(比如不能直接操作window、document)。
  • 适合做大量计算、数据处理、图像处理等。

5. Web Worker vs 主线程

比较项 主线程 Web Worker
线程数 单线程 独立多线程
DOM访问 可以直接访问DOM 不能访问DOM,只能通信
阻塞风险 大量计算会阻塞UI 不阻塞UI,后台运行
通信方式 直接操作变量/函数 通过 postMessage 传递消息
使用场景 UI渲染、事件响应 复杂计算、数据处理