今天來聊聊 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 編輯過