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