2021年7月25日 星期日

jQuery同步Ajax阻塞問題及解決辦法


深入探討 jQuery 同步 Ajax 請求的阻塞問題與解方

在 Web 開發中,非同步 JavaScript 與 XML (Ajax) 技術的出現極大地提升了使用者體驗,使得網頁無需重新載入即可與伺服器交換資料。然而,在某些情況下,開發者可能會遇到或曾經使用過同步 Ajax 請求。本文將深入解析 jQuery 中同步 Ajax 請求的行為模式、其帶來的阻塞問題,並提供一系列現代化、專業的解決方案。


同步 Ajax 請求的本質與阻塞效應

傳統上,JavaScript 在瀏覽器中是單執行緒的。這意味著在任何給定時間點,瀏覽器只能執行一個 JavaScript 任務。當我們談論 同步 Ajax 請求 時,其核心行為特點是:

  1. 程式碼執行暫停:一旦發出同步 Ajax 請求,瀏覽器的 JavaScript 執行緒會被完全阻塞 (blocked)。程式碼的執行會停止,直到伺服器返回回應。

  2. 使用者介面凍結:由於 JavaScript 執行緒與瀏覽器的渲染執行緒通常共享同一資源,當 JavaScript 執行緒被阻塞時,使用者介面 (UI) 也會隨之凍結。這表示使用者無法點擊按鈕、輸入文字、滾動頁面等,直到同步請求完成。這導致了糟糕的使用者體驗。

  3. 潛在的「無響應腳本」警告:如果同步請求的處理時間過長(例如數秒),瀏覽器可能會偵測到腳本長時間無響應,進而彈出「無響應腳本」警告,詢問使用者是否終止腳本,這對用戶來說極具破壞性。

在 jQuery 中,透過將 async 選項設置為 false 來發起同步 Ajax 請求:

JavaScript
$.ajax({
    url: "/api/data",
    async: false, // 同步請求的核心設定
    success: function(data) {
        console.log("資料已成功接收 (同步):", data);
    },
    error: function(jqXHR, textStatus, errorThrown) {
        console.error("請求失敗:", textStatus, errorThrown);
    }
});
console.log("此行代碼會在 Ajax 請求完成後才執行。");

為何會出現同步 Ajax 的需求?(以及為何應避免)

儘管存在嚴重缺陷,同步 Ajax 請求在某些特定場景下仍然被少數開發者考慮:

  • 程式碼簡潔性錯覺:在某些簡單的循序邏輯中,同步請求使得程式碼看起來更為直觀,避免了回呼地獄 (callback hell) 或 Promise 鏈的複雜性。

  • 遺留系統或特定限制:在某些舊有的系統架構中,或為了與某些特定 API 交互,開發者可能被限制使用同步方式。

  • 避免複雜的非同步流程管理:對於不熟悉非同步編程模型(例如 Promise, Async/Await)的開發者,同步請求似乎提供了一種逃避複雜性的途徑。

然而,這些「需求」幾乎總能透過更現代、更優雅的非同步模式來解決,並且避開同步請求所帶來的巨大負面影響。 在絕大多數情況下,應當堅決避免在前端(特別是瀏覽器環境)使用同步 Ajax 請求。


解決同步 Ajax 阻塞問題的專業方案

解決同步 Ajax 引起的阻塞問題,核心在於擁抱非同步編程模型。以下是幾種現代化且廣泛採用的解決方案:

1. 使用 jQuery 的 Promise/Deferred 物件 (版本 1.5+)

jQuery 的 $.ajax() 方法本身就返回一個 Promise-like 的 Deferred 物件。這使得你可以使用 .done(), .fail(), .always() 等方法來處理請求結果,而無需使用 async: false

JavaScript
$.ajax({
    url: "/api/data",
    method: "GET" // 預設 async 為 true,通常無需明確設定
})
.done(function(data) {
    console.log("資料已成功接收 (非同步):", data);
})
.fail(function(jqXHR, textStatus, errorThrown) {
    console.error("請求失敗:", textStatus, errorThrown);
})
.always(function() {
    console.log("請求完成,無論成功或失敗。");
});
console.log("此行代碼會立即執行,不會等待 Ajax 請求。");

此方法是從 jQuery 1.5 版本開始引入的,有效解決了「回呼地獄」的一部分問題,使得多個非同步操作的鏈接和錯誤處理更為清晰。

2. 使用 ES6 原生 Promise (推薦)

隨著 ES6 (ECMAScript 2015) 的普及,原生的 Promise 物件成為處理非同步操作的標準和首選方式。它提供了更強大、更靈活的非同步流程控制能力,且是現代 JavaScript 開發的基石。雖然 jQuery 的 Deferred 物件與 Promise 相似,但在現代專案中,通常會直接使用原生 Promise。

你可以將 $.ajax() 請求包裝在一個原生 Promise 中,或者直接使用基於 Promise 的 Fetch API。

JavaScript
// 方法一:將 jQuery Ajax 包裝為原生 Promise
function fetchDataWithPromise() {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: "/api/data",
            method: "GET",
            success: function(data) {
                resolve(data);
            },
            error: function(jqXHR, textStatus, errorThrown) {
                reject({ textStatus, errorThrown });
            }
        });
    });
}

fetchDataWithPromise()
    .then(data => {
        console.log("資料已成功接收 (原生 Promise):", data);
    })
    .catch(error => {
        console.error("請求失敗:", error.textStatus, error.errorThrown);
    });

console.log("此行代碼會立即執行,不會等待 Promise 請求。");

3. 使用 ES2017 async/await (最佳實踐)

async/await 是基於 Promise 的語法糖,它使得非同步程式碼的編寫和閱讀更像同步程式碼,極大地簡化了非同步流程的處理。這是目前處理非同步操作的最佳實踐

JavaScript
// 假設 fetchDataWithPromise 是上述包裝好的 Promise 函數
async function getDataAndProcess() {
    try {
        const data = await fetchDataWithPromise(); // 等待 Promise 完成
        console.log("資料已成功接收 (Async/Await):", data);
        // 在這裡可以安全地使用 data 進行後續同步操作
    } catch (error) {
        console.error("請求失敗 (Async/Await):", error.textStatus, error.errorThrown);
    } finally {
        console.log("Async/Await 流程完成。");
    }
}

getDataAndProcess();
console.log("此行代碼會立即執行,不會等待 Async/Await 函數內部。");

使用 async/await,開發者可以以一種更直觀的方式組織複雜的非同步邏輯,避免了回呼的嵌套和 Promise 鏈的過度冗長,同時保持了非阻塞的 UI。


結論

在現代 Web 開發中,同步 Ajax 請求應當被視為一種反模式 (anti-pattern)。其帶來的 UI 阻塞問題和糟糕的使用者體驗是不可接受的。開發者應當積極擁抱非同步編程模型,利用 jQuery 的 Promise/Deferred、原生的 Promise 或更進一步的 async/await 來處理網路請求。

這些非同步解決方案不僅能避免 UI 阻塞,還能讓程式碼更加健壯、可讀且易於維護,從而構建出高效、流暢且具有良好響應能力的 Web 應用程式。將非同步思維融入開發實踐,是提升前端開發水準的關鍵一步。

熱門文章