今天來聊聊 JavaScript 中的異步編程,篇幅略微有點長。
異步編程是相對高級的內(nèi)容,對于初學(xué)者來說,如果不能完全理解也沒有關(guān)系,后續(xù)可以再來復(fù)習(xí)。做到盡量理解這里面的知識點就好。
同步編程 vs 異步編程
首先,我們來看看什么是同步編程和異步編程。
在同步編程中,代碼是按順序執(zhí)行的。
也就是每一行代碼都會等待前一行代碼執(zhí)行完畢后再執(zhí)行。比如:
console.log('第一步'); console.log('第二步'); console.log('第三步');
在這個例子中,輸出的順序是固定的。即,第一步 -> 第二步 -> 第三步。
但在異步編程中,某些操作可以在后臺執(zhí)行,而不會阻塞主線程。
換句話說,輸出的順序和代碼順序不完全一樣。
在處理一些比較耗時的操作,比如如網(wǎng)絡(luò)請求、文件讀取等,有助于提高效率。
console.log('第一步'); setTimeout(() => { console.log('第二步'); }, 1000); console.log('第三步');
在這個例子中,代碼順序和前面一樣,但輸出的順序是:第一步 -> 第三步 -> 第二步。
這是因為 setTimeout
是一個異步操作函數(shù),它不會阻塞主線程,而是會在 1 秒后執(zhí)行回調(diào)函數(shù)。
至于什么是回調(diào)函數(shù),一會兒再細(xì)說。
為什么需要異步編程
所以,異步編程的主要目的是提高程序的效率,避免阻塞主線程。
假如在一個網(wǎng)頁中發(fā)起一個網(wǎng)絡(luò)請求,而這個請求需要幾秒鐘才能完成的話。
如果使用同步編程,整個網(wǎng)頁在這幾秒鐘內(nèi)都會被阻塞,也就是看起來像卡住了一樣,用戶無法進(jìn)行任何操作。
在如今這個網(wǎng)絡(luò)和服務(wù)器處理能力如此強(qiáng)大的情況下,這顯然是不能被接受的。
那有什么方式來解決這個問題呢?
答案就是異步編程。
而異步編程的實現(xiàn),也有幾種不同的方式,一個一個來看。
使用回調(diào)函數(shù)
回調(diào)函數(shù)是最基本的異步編程方式。
它們允許你在異步操作完成后執(zhí)行某些代碼。比如:
//定義函數(shù)fetchData function fetchData(callback) { setTimeout(() => { const data = '數(shù)據(jù)加載完成'; callback(data); }, 2000); } console.log('開始加載數(shù)據(jù)'); //調(diào)用函數(shù)fetchData fetchData((data) => { console.log(data); }); console.log('繼續(xù)執(zhí)行其他操作');
在這個例子中,我們定義了一個 fetchData
函數(shù),它接受一個回調(diào)函數(shù)作為參數(shù)。
在 2 秒后,回調(diào)函數(shù)會被調(diào)用,表示數(shù)據(jù)加載完成。
輸出的順序是:開始加載數(shù)據(jù) -> 繼續(xù)執(zhí)行其他操作 -> 數(shù)據(jù)加載完成。
使用 Promise
Promise 是另一種處理異步操作的方式。
它可以讓我們更優(yōu)雅地處理異步操作,避免回調(diào)地獄。
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = '數(shù)據(jù)加載完成'; resolve(data); }, 2000); }); } console.log('開始加載數(shù)據(jù)'); fetchData() .then((data) => { console.log(data); }) .catch((error) => { console.error(error); }); console.log('繼續(xù)執(zhí)行其他操作');
在這個例子中,我們定義了一個 fetchData
函數(shù),它返回一個 Promise。
在 2 秒后,Promise 會被 resolve,表示數(shù)據(jù)加載完成。
輸出的順序是:開始加載數(shù)據(jù) -> 繼續(xù)執(zhí)行其他操作 -> 數(shù)據(jù)加載完成。
使用 async/await
async
和 await
是基于 Promise 的語法糖,使異步代碼看起來更像同步代碼,更易讀易寫。
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { const data = '數(shù)據(jù)加載完成'; resolve(data); }, 2000); }); } async function loadData() { console.log('開始加載數(shù)據(jù)'); try { const data = await fetchData(); console.log(data); } catch (error) { console.error(error); } console.log('繼續(xù)執(zhí)行其他操作'); } loadData();
在這個例子中,我們定義了一個 loadData
異步函數(shù),并在其中使用 await
來等待 fetchData
的結(jié)果。
輸出的順序是:開始加載數(shù)據(jù) -> 數(shù)據(jù)加載完成 -> 繼續(xù)執(zhí)行其他操作。
使用 Web Workers
Web Workers 是另一種處理異步操作的方式,它允許我們在后臺線程中執(zhí)行代碼,而不會阻塞主線程。
JavaScript 是單線程的,這意味著它一次只能執(zhí)行一個任務(wù)。
如果一個任務(wù)耗時較長,整個應(yīng)用程序的響應(yīng)速度就會變慢,甚至?xí)霈F(xiàn)卡頓現(xiàn)象。
Web Workers 允許我們在主線程之外創(chuàng)建獨立的工作線程來處理耗時的任務(wù),從而避免阻塞主線程,提高應(yīng)用程序的性能和用戶體驗。
使用 Web Workers 的具體場景大概有如下:
處理計算密集型任務(wù):例如復(fù)雜的數(shù)學(xué)計算、圖像處理等,這些任務(wù)可以放在 Web Worker 中執(zhí)行,從而避免阻塞主線程。
處理大數(shù)據(jù):在處理大量數(shù)據(jù)時,可以將數(shù)據(jù)處理任務(wù)交給 Web Worker,從而保持主線程的流暢運行。
文件處理:例如讀取和解析大文件,可以使用 Web Worker 來處理文件流,避免主線程卡頓。
WebSocket 消息處理:在處理 WebSocket 消息時,可以使用 Web Worker 來處理接收到的消息,從而提高消息處理的效率。
Web Workers 的使用也是有限制的,如下:
同源限制:Worker 線程執(zhí)行的腳本文件必須與主線程的文件同源。
文件限制:Worker 線程無法讀取本地文件,文件需要通過主線程讀取后再傳輸給 Worker。
DOM 操作限制:Worker 線程無法直接操作 DOM 對象,但可以通過消息傳遞與主線程通信。
代碼示例
首先,我們需要創(chuàng)建一個 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('計算結(jié)果:', event.data); }; console.log('發(fā)送數(shù)據(jù)到 worker'); worker.postMessage(10); console.log('繼續(xù)執(zhí)行其他操作');
在這個例子中,我們創(chuàng)建了一個 worker,并向它發(fā)送數(shù)據(jù)。
worker 會在后臺線程中處理數(shù)據(jù),并將結(jié)果返回給主線程。
輸出的順序是:發(fā)送數(shù)據(jù)到 worker -> 繼續(xù)執(zhí)行其他操作 -> 計算結(jié)果。
異步迭代器和生成器
異步迭代器和生成器允許我們在異步操作中使用 for...of
循環(huán)。
異步迭代器和生成器使得在不阻塞代碼執(zhí)行的情況下遍歷數(shù)據(jù)或執(zhí)行任務(wù)成為可能。
比如,當(dāng)我們通過網(wǎng)絡(luò)一塊一塊地下載數(shù)據(jù)時,異步迭代器可以讓我們在每次數(shù)據(jù)塊到達(dá)時處理它,而不必等待所有數(shù)據(jù)都下載完畢。
這種方式特別適用于處理流式數(shù)據(jù)或分頁數(shù)據(jù)。
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
,它每秒生成一個數(shù)字。
然后,我們使用 for await...of
循環(huán)來迭代生成器的值。
實際應(yīng)用例子
幾個常見的應(yīng)用場景例子,代碼看不懂目前也沒有關(guān)系,只要明白有這個場景的應(yīng)用目前就足夠了。
1. 處理分頁數(shù)據(jù) 在處理分頁數(shù)據(jù)時,異步迭代器可以幫助我們逐頁獲取數(shù)據(jù)并進(jìn)行處理,而不需要一次性加載所有數(shù)據(jù)。
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. 處理文件流 異步迭代器可以用于逐行讀取大文件,而不需要一次性將整個文件加載到內(nèi)存中。
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); } })();
錯誤處理
在異步編程中,錯誤處理尤為重要,特別是在問題調(diào)查中。
在回調(diào)函數(shù)、Promise 和 async/await
中要考慮處理錯誤,確保代碼的健壯性。
比如在使用 async/await
時,我們可以使用 try...catch
來捕獲錯誤:
async function loadData() { console.log('開始加載數(shù)據(jù)'); try { const data = await fetchData(); console.log(data); } catch (error) { console.error('加載數(shù)據(jù)時出錯:', error); } console.log('繼續(xù)執(zhí)行其他操作'); } loadData();
總結(jié)
?? 盡量使用 Promise 和 async/await 來處理異步操作,因為它們比回調(diào)函數(shù)更易讀易維護(hù)。
?? 在異步編程中,錯誤處理尤為重要。使用 try...catch 塊來捕獲 async/await 中的錯誤,使用 .catch() 方法來處理 Promise 中的錯誤。
?? 對于計算密集型任務(wù),可以使用 Web Workers 在后臺線程中執(zhí)行代碼,避免阻塞主線程。
該文章在 2024/10/28 16:28:52 編輯過