今天来聊聊 JavaScript 中的异步编程,篇幅略微有点长。
异步编程是相对高级的内容,对于初学者来说,如果不能完全理解也没有关系,后续可以再来复习。做到尽量理解这里面的知识点就好。
同步编程 vs 异步编程
首先,我们来看看什么是同步编程和异步编程。
在同步编程中,代码是按顺序执行的。
也就是每一行代码都会等待前一行代码执行完毕后再执行。比如:
console.log('第一步'); console.log('第二步'); console.log('第三步');
在这个例子中,输出的顺序是固定的。即,第一步 -> 第二步 -> 第三步。
但在异步编程中,某些操作可以在后台执行,而不会阻塞主线程。
换句话说,输出的顺序和代码顺序不完全一样。
在处理一些比较耗时的操作,比如如网络请求、文件读取等,有助于提高效率。
console.log('第一步'); setTimeout(() => { console.log('第二步'); }, 1000); console.log('第三步');
在这个例子中,代码顺序和前面一样,但输出的顺序是:第一步 -> 第三步 -> 第二步。
这是因为 setTimeout
是一个异步操作函数,它不会阻塞主线程,而是会在 1 秒后执行回调函数。
至于什么是回调函数,一会儿再细说。
为什么需要异步编程
所以,异步编程的主要目的是提高程序的效率,避免阻塞主线程。
假如在一个网页中发起一个网络请求,而这个请求需要几秒钟才能完成的话。
如果使用同步编程,整个网页在这几秒钟内都会被阻塞,也就是看起来像卡住了一样,用户无法进行任何操作。
在如今这个网络和服务器处理能力如此强大的情况下,这显然是不能被接受的。
那有什么方式来解决这个问题呢?
答案就是异步编程。
而异步编程的实现,也有几种不同的方式,一个一个来看。
使用回调函数
回调函数是最基本的异步编程方式。
它们允许你在异步操作完成后执行某些代码。比如:
//定义函数fetchData function fetchData(callback) { setTimeout(() => { const data = '数据加载完成'; callback(data); }, 2000); } console.log('开始加载数据'); //调用函数fetchData fetchData((data) => { console.log(data); }); console.log('继续执行其他操作');
在这个例子中,我们定义了一个 fetchData
函数,它接受一个回调函数作为参数。
在 2 秒后,回调函数会被调用,表示数据加载完成。
输出的顺序是:开始加载数据 -> 继续执行其他操作 -> 数据加载完成。
使用 Promise
Promise 是另一种处理异步操作的方式。
它可以让我们更优雅地处理异步操作,避免回调地狱。
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = '数据加载完成'; resolve(data); }, 2000); }); } console.log('开始加载数据'); fetchData() .then((data) => { console.log(data); }) .catch((error) => { console.error(error); }); console.log('继续执行其他操作');
在这个例子中,我们定义了一个 fetchData
函数,它返回一个 Promise。
在 2 秒后,Promise 会被 resolve,表示数据加载完成。
输出的顺序是:开始加载数据 -> 继续执行其他操作 -> 数据加载完成。
使用 async/await
async
和 await
是基于 Promise 的语法糖,使异步代码看起来更像同步代码,更易读易写。
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = '数据加载完成'; resolve(data); }, 2000); }); } async function loadData() { console.log('开始加载数据'); try { const data = await fetchData(); console.log(data); } catch (error) { console.error(error); } console.log('继续执行其他操作'); } loadData();
在这个例子中,我们定义了一个 loadData
异步函数,并在其中使用 await
来等待 fetchData
的结果。
输出的顺序是:开始加载数据 -> 数据加载完成 -> 继续执行其他操作。
使用 Web Workers
Web Workers 是另一种处理异步操作的方式,它允许我们在后台线程中执行代码,而不会阻塞主线程。
JavaScript 是单线程的,这意味着它一次只能执行一个任务。
如果一个任务耗时较长,整个应用程序的响应速度就会变慢,甚至会出现卡顿现象。
Web Workers 允许我们在主线程之外创建独立的工作线程来处理耗时的任务,从而避免阻塞主线程,提高应用程序的性能和用户体验。
使用 Web Workers 的具体场景大概有如下:
处理计算密集型任务:例如复杂的数学计算、图像处理等,这些任务可以放在 Web Worker 中执行,从而避免阻塞主线程。
处理大数据:在处理大量数据时,可以将数据处理任务交给 Web Worker,从而保持主线程的流畅运行。
文件处理:例如读取和解析大文件,可以使用 Web Worker 来处理文件流,避免主线程卡顿。
WebSocket 消息处理:在处理 WebSocket 消息时,可以使用 Web Worker 来处理接收到的消息,从而提高消息处理的效率。
Web Workers 的使用也是有限制的,如下:
同源限制:Worker 线程执行的脚本文件必须与主线程的文件同源。
文件限制:Worker 线程无法读取本地文件,文件需要通过主线程读取后再传输给 Worker。
DOM 操作限制:Worker 线程无法直接操作 DOM 对象,但可以通过消息传递与主线程通信。
代码示例
首先,我们需要创建一个 worker 脚本 worker.js
:
// worker.js self.onmessage = function (event) { const result = event.data * 2; self.postMessage(result); };
然后,在主线程中使用这个 worker:
const worker = new Worker('worker.js'); worker.onmessage = function (event) { console.log('计算结果:', event.data); }; console.log('发送数据到 worker'); worker.postMessage(10); console.log('继续执行其他操作');
在这个例子中,我们创建了一个 worker,并向它发送数据。
worker 会在后台线程中处理数据,并将结果返回给主线程。
输出的顺序是:发送数据到 worker -> 继续执行其他操作 -> 计算结果。
异步迭代器和生成器
异步迭代器和生成器允许我们在异步操作中使用 for...of
循环。
异步迭代器和生成器使得在不阻塞代码执行的情况下遍历数据或执行任务成为可能。
比如,当我们通过网络一块一块地下载数据时,异步迭代器可以让我们在每次数据块到达时处理它,而不必等待所有数据都下载完毕。
这种方式特别适用于处理流式数据或分页数据。
async function* asyncGenerator() { for (let i = 0; i < 3; i++) { await new Promise((resolve) => setTimeout(resolve, 1000)); yield i; } } (async () => { for await (let num of asyncGenerator()) { console.log(num); } })();
在这个例子中,我们定义了一个异步生成器 asyncGenerator
,它每秒生成一个数字。
然后,我们使用 for await...of
循环来迭代生成器的值。
实际应用例子
几个常见的应用场景例子,代码看不懂目前也没有关系,只要明白有这个场景的应用目前就足够了。
1. 处理分页数据 在处理分页数据时,异步迭代器可以帮助我们逐页获取数据并进行处理,而不需要一次性加载所有数据。
async function* fetchPages(url) { let page = 1; while (true) { const response = await fetch(`${url}?page=${page}`); const data = await response.json(); if (data.length === 0) break; yield data; page++; } } (async () => { for await (let pageData of fetchPages('https://api.xxx.com/items')) { console.log(pageData); } })();
2. 处理文件流 异步迭代器可以用于逐行读取大文件,而不需要一次性将整个文件加载到内存中。
const fs = require('fs'); const readline = require('readline'); async function* readLines(filePath) { const fileStream = fs.createReadStream(filePath); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity, }); for await (const line of rl) { yield line; } } (async () => { for await (let line of readLines('largefile.txt')) { console.log(line); } })();
3. 处理 WebSocket 消息 在处理 WebSocket 消息时,异步迭代器可以帮助我们逐条处理接收到的消息。
async function* receiveMessages(socket) { socket.onmessage = (event) => { socket.queue.push(event.data); }; socket.queue = []; while (true) { if (socket.queue.length > 0) { yield socket.queue.shift(); } else { await new Promise((resolve) => setTimeout(resolve, 100)); } } } (async () => { const socket = new WebSocket('wss://example.com/socket'); for await (let message of receiveMessages(socket)) { console.log(message); } })();
错误处理
在异步编程中,错误处理尤为重要,特别是在问题调查中。
在回调函数、Promise 和 async/await
中要考虑处理错误,确保代码的健壮性。
比如在使用 async/await
时,我们可以使用 try...catch
来捕获错误:
async function loadData() { console.log('开始加载数据'); try { const data = await fetchData(); console.log(data); } catch (error) { console.error('加载数据时出错:', error); } console.log('继续执行其他操作'); } loadData();
总结
🍑 尽量使用 Promise 和 async/await 来处理异步操作,因为它们比回调函数更易读易维护。
🍑 在异步编程中,错误处理尤为重要。使用 try...catch 块来捕获 async/await 中的错误,使用 .catch() 方法来处理 Promise 中的错误。
🍑 对于计算密集型任务,可以使用 Web Workers 在后台线程中执行代码,避免阻塞主线程。
该文章在 2024/10/28 16:28:52 编辑过