JavaScript 非同步程式設計入門

FunTester發表於2024-10-12

最近開始不斷學習實踐 JavaScript ,出於效能測試的敏感,首先研究了 JavaScript 的非同步程式設計實踐,目前看跟之前學過的 Java 和 Go 都不太一樣。使用的語法上相對更加複雜,也可能因為我實踐得還太少。

非同步程式設計

非同步程式設計是一種在程式執行過程中,不阻塞主執行緒的任務處理方式。相較於同步程式設計,非同步程式設計允許程式在等待某個任務(例如網路請求或檔案讀寫)完成的同時,繼續執行其他操作。這種方式極大提高了程式的效率,尤其在處理大量 I/O 操作時,表現尤為突出。

在 JavaScript 中,非同步程式設計尤為重要,原因在於它的單執行緒特性。JavaScript 在瀏覽器環境下執行時,只有一個主執行緒負責執行程式碼。如果程式中某個任務需要較長時間才能完成(比如網路請求、資料庫查詢等),而沒有采用非同步處理方式,主執行緒將會被阻塞,導致整個頁面的互動變得遲緩甚至無響應。

為了避免這種情況,JavaScript 提供了多種非同步處理方式,如回撥函式Promiseasync/await等。透過這些機制,JavaScript 能夠在處理耗時任務時,不阻塞主執行緒,保持頁面的流暢性和響應性。

回撥函式(Callback)

回撥函式是指作為引數傳遞給另一個函式,並在該函式執行完畢後呼叫的函式。在 JavaScript 的非同步程式設計中,回撥函式是最早且最基礎的實現方式之一。當某個非同步操作(如網路請求或定時器)完成時,JavaScript 執行時環境會呼叫提供的回撥函式,繼續執行後續的邏輯。這種模式允許我們在非同步任務完成後進行處理,而不阻塞主執行緒。

在 JavaScript 中,回撥函式透過結合瀏覽器或 Node.js 的事件迴圈機制來實現非同步行為。單單使用 JavaScript 本身無法實現非同步,而是透過將任務交給瀏覽器或 Node.js 的執行時(如定時器、I/O 操作、網路請求等)來處理,等這些任務完成後,再透過回撥函式把結果傳回給 JavaScript 主執行緒繼續執行。這整個過程就是實現非同步的關鍵。

示例

function fetchData(callback) {  
    console.log("Fetching data...");  
    setTimeout(() => {// 模擬耗時操作  
        // 模擬耗時操作  
        const data = "Hello FunTester!";  
        callback(data); // 呼叫回撥函式,傳遞資料  
    }, 1000);  
}  

function processData(data) {  
    console.log("Data received:", data);// 處理資料的邏輯  
}  

console.info("start --------");  
fetchData(processData);// 呼叫 fetchData 函式,傳入回撥函式  
console.info("end --------");

在這個例子中,fetchData 模擬了一個非同步資料獲取的操作,執行完成後呼叫回撥函式 processData 來處理獲取到的資料。控制檯列印:

start --------
Fetching data...
end --------
Data received: Hello FunTester!

Promise

Promise 是一種用於處理非同步操作的物件,它代表了一個非同步操作的最終完成(或失敗)及其結果值。Promise 提供了更清晰、更可讀的方式來管理多個非同步操作,特別是當它們相互依賴時。相比回撥函式,Promise 能避免回撥地獄,使程式碼結構更加扁平化和易於維護。

Promise 的三種狀態

  1. Pending(待定):初始狀態,表示非同步操作尚未完成,也沒有結果。
  2. Fulfilled(已完成):非同步操作成功完成,並返回一個值。
  3. Rejected(已拒絕):非同步操作失敗,並返回一個原因。

在 Promise 的生命週期中,它只能從 pending 狀態轉移到 fulfilledrejected 狀態,並且一旦狀態發生變化,結果就不會再改變。

Promise 的基本用法

一個 Promise 物件會接受一個函式作為引數,該函式本身又有兩個引數:resolvereject。當非同步操作成功時,呼叫 resolve 來將 Promise 狀態改為 fulfilled;當操作失敗時,呼叫 reject 將狀態改為 rejected

.then().catch() 的用法

  • .then():用於處理 Promise 成功完成(fulfilled)後的結果。你可以在 .then() 中執行後續操作,鏈式呼叫。
  • .catch():用於捕獲 Promise 的錯誤(rejected),並處理異常情況。

Promise 示例

const fetchData = async () => {  
    return new Promise((resolve, reject) => {  
        setTimeout(() => {  
            const success = true; // 模擬成功或失敗  
            if (success) {// 成功, 呼叫 resolve, 並傳遞資料  
                resolve("hello, FunTester");  
            } else {// 失敗, 呼叫 reject, 並傳遞錯誤資訊  
                reject("sorry, error occurred");  
            }  
        });  
    });  
};  

console.info("start--------------");  
// 使用 Promise 來處理非同步操作  
fetchData()  
    .then((data) => {// 成功時的處理  
        console.log("received:", data);  
        return data.length;  
    })  
    .then((age) => {// 成功時的處理  
        console.log("length:", age);  
    })  
    .catch((error) => {// 失敗時的處理  
        console.error("Error:", error);  
    });  

console.info("end--------------");

控制檯列印:

start--------------
end--------------
received: hello, FunTester
length: 16

下面是一些小小的 tips:

  • Promise 提供了一種更加簡潔、優雅的方式來處理非同步操作,避免回撥地獄。
  • 它有三種狀態:pending(待定)、fulfilled(已完成)、rejected(已拒絕)。
  • 使用 .then() 處理成功結果,使用 .catch() 捕獲和處理錯誤。
  • 多個非同步操作可以鏈式呼叫,提升了程式碼的可讀性和維護性。

async/await

async/await 是 ES2017(ES8)引入的語法,提供了一種更加簡潔的方式來處理非同步程式碼,使其看起來更像同步程式碼。這種方式可以減少 Promise 鏈的複雜性,提升程式碼的可讀性和維護性。async/await 實際上是基於 Promises 之上的語法糖。

透過使用 asyncawait,我們可以編寫看起來像同步執行的程式碼,但它在內部仍然是非同步的。這種寫法可以避免 Promise 鏈式呼叫的繁瑣結構,將非同步任務的執行流順序化,邏輯更加清晰。

async/await 的基本用法

  1. async 函式:宣告一個 async 函式,它會自動返回一個 Promise。即使沒有顯式返回 Promise,async 函式預設會把返回值封裝成一個 Promise。
  2. await 表示式:用於等待一個 Promise 的結果。它暫停 async 函式的執行,直到 Promise 解決(resolve)或拒絕(reject),然後繼續執行後續程式碼。

示例:使用 async/await

const fetchData = () => {  
    return new Promise((resolve, reject) => {  
        setTimeout(() => {// 模擬非同步操作  
            const success = true;  
            if (success) {  
                resolve("hello funtester");  
            } else {  
                reject("sorry, error");  
            }  
        }, 1000);  
    });  
};  

async function getData() {  
    try {  
        const data = await fetchData(); // 等待 Promise 解決  
        console.log("Data received:", data);  
    } catch (error) {  
        console.error("Error:", error); // 處理錯誤  
    }  
}  
console.info("start----------------");  
let data = getData();  
console.log("data:", data);  
console.info("end----------------");

控制檯列印:

start----------------
data: Promise { <pending> }
end----------------
Data received: hello funtester

在使用 async/await 時,錯誤處理的方式類似於同步程式碼中捕獲異常。我們可以使用 try/catch 塊來處理非同步操作中的異常,這種方式比在每個 .then() 末尾新增 .catch() 更加直觀。

async/await 基於 Promise

需要注意的是,async/await 並不是替代 Promise 的機制,而是基於 Promise 進行的更高階抽象。它們簡化了 Promise 的鏈式呼叫,消除了巢狀結構。可以把 await 視作一個暫停點,等待 Promise 完成(無論是成功還是失敗),使得非同步程式碼的處理更符合程式的執行邏輯。

非同步程式設計的重要性

在 JavaScript 中,非同步程式設計至關重要,因為 JavaScript 執行在單執行緒環境中,尤其是在瀏覽器和 Node.js 等平臺中。單執行緒意味著同一時間只能執行一個任務。如果程式碼中的某些操作(如網路請求、檔案讀取或定時任務)需要較長的執行時間,而 JavaScript 只支援同步程式設計的話,整個執行緒將被阻塞,使用者介面會變得卡頓或無響應。因此,非同步程式設計透過不阻塞主執行緒來解決這個問題,使得 JavaScript 能夠高效處理 I/O 密集型任務,保持應用的流暢執行。

當然 JavaScript 非同步程式設計的內容還有其他高階用法,比如學習 Promise.all、Promise.race 等,將來等我實踐充足再來分享。

FunTester 原創精華
  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章