今天,我們要暫時停下手中修改 UI 的工作,來挑戰一個在 JavaScript 世界中,既是「最強大的盟友」,也是「最難纏的敵人」的概念。
回顧一下我們的進度:我們的 UI 工廠已經完美竣工,介面美觀,元件蓄勢待發。我們滿心期待地準備在 scanButton.onclick
事件中,寫下呼叫藍牙 API 的第一行程式碼。
但這裡有一個我們無法迴避的問題:當我們命令瀏覽器「去掃描藍牙裝置」時,這個過程需要多久?一秒?十秒?還是因為附近沒有裝置而永遠不會結束?
如果我們的 JavaScript 程式碼像過去一樣,一行一行地「同步」執行,那麼當它執行到掃描指令時,它就會停在那裡死等,直到掃描結束。這會導致什麼後果?
整個網頁將會完全凍結!
使用者無法點擊任何按鈕,無法滾動頁面,動畫會卡住,整個介面將如同一張死去的圖片。這是絕對無法接受的使用者體驗。
為了解決這個問題,我們必須學習一種全新的程式設計思維模式——非同步程式設計 (Asynchronous Programming)。這或許是新手學習 JS 時遇到的第一個、也是最重要的一個坎。但請相信我,一旦你跨越了它,你將解開 Web Bluetooth API 乃至所有現代網頁 API 的最終封印。今天,就是我們修煉絕世武功的關鍵時刻。
讓我們用一個「點咖啡」的例子來理解。
流程:你走到櫃檯,跟咖啡師說:「我要一杯拿鐵」。然後,你就必須站在櫃檯前,眼睜睜地看著他磨豆、壓粉、打奶泡、拉花... 直到他把咖啡交到你手上,你才能離開去做別的事。在你等待的過程中,你身後的客人也只能乾等,什麼都做不了。
優點:流程簡單,順序固定,很好理解。
缺點:效率極低。如果前面的人點了十杯特調,後面所有人都要被「阻塞 (Blocking)」。
程式世界的體現:這就是我們到目前為止寫程式的方式。A(); B(); C();
,B
必須等 A
完全結束才能開始,C
也必須等 B
結束。
流程:你走到櫃檯,說:「我要一杯拿鐵」。咖啡師收了錢,然後遞給你一個會震動的取餐器,並告訴你:「好了會叫你,你可以先去找位子坐,滑滑手機」。你拿著取餐器(一個承諾 Promise),自由地去做自己的事。咖啡師則繼續接受下一位客人的點單。幾分鐘後,你的取餐器震動了(承諾兌現了),你憑著它去領取你的咖啡。
優點:效率極高。等待咖啡製作(一個耗時的操作)的過程中,你(主程式)沒有被阻塞,可以繼續做其他事(保持網頁流暢)。
缺點:流程比同步複雜,你需要知道如何處理那個「取餐器」。
程式世界的體現:A(); B(); C();
,如果 A
是一個非同步操作(例如掃描藍牙),JS 會立刻開始執行 A
,然後不等它結束,就馬上接著去執行 B
和 C
。當未來的某個時刻 A
完成了,它會透過一個特殊機制通知我們。
在現代 JavaScript 中,那個「取餐器」就是 Promise 物件。
當你呼叫一個非同步 API (例如 navigator.bluetooth.requestDevice()
) 時,它不會立刻回傳結果。相反,它會立刻回傳一個 Promise 物件。這個 Promise 就像一張收據,上面寫著「我承諾,未來會給你一個結果」。
一個 Promise 有三種狀態:
Pending (等待中):初始狀態。咖啡還在做。
Fulfilled (已實現):操作成功完成。咖啡做好了!你會得到承諾的結果(例如掃描到的藍牙裝置物件)。
Rejected (已拒絕):操作失敗。咖啡豆沒了!你會得到一個失敗的原因(例如使用者取消了掃描視窗)。
async/await
:處理 Promise 最優雅的方式那麼,我們該如何處理這個 Promise「取餐券」呢?雖然有 .then()
和 .catch()
的傳統方法,但現代 JavaScript 提供了一種更直觀、更像同步程式碼的語法糖——async/await
。
這兩個關鍵字必須成對使用:
async
:用在 function
關鍵字前面。它像是在給一個函式貼上標籤:「注意!這個函式內部可能有需要等待的非同步操作。」 async
函式本身會自動回傳一個 Promise。
await
:只能用在 async
函式內部。它像是在說:「暫停!請在這裡暫停函式的執行,直到我後面這個 Promise (await
後面的非同步操作) 完成為止。完成後,再把它的結果交給我,然後繼續往下執行。」
try...catch
語法)讓我們用 async/await
來改寫點咖啡的流程,並處理可能發生的意外(咖啡賣完了)。
// 這是我們昨天寫好的事件監聽器
// 現在我們要把它升級成 async 函式
scanButton.onclick = async () => {
try {
// 1. 更新 UI,告訴使用者我們正在掃描
statusText.textContent = '正在掃描藍牙裝置...';
console.log('開始掃描...');
// 2. 呼叫非同步 API (這是我們明天的內容)
// await 會在這裡「暫停」onclick 函式的執行,但不會凍結整個網頁!
// 它會等待 navigator.bluetooth.requestDevice() 這個 Promise 完成
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true // 暫時允許所有裝置
});
// 3. 如果 await 成功,Promise 狀態變為 Fulfilled,device 物件會被回傳
// 程式碼會從這裡繼續往下執行
statusText.textContent = `找到裝置: ${device.name}`;
console.log(`找到裝置: ${device.name} (ID: ${device.id})`);
// TODO: 連接裝置...
} catch (error) {
// 4. 如果 await 失敗 (例如使用者點了「取消」),Promise 狀態變為 Rejected
// 程式碼會立刻跳到 catch 區塊
statusText.textContent = '掃描失敗或已取消';
console.error('掃描時發生錯誤:', error);
}
};
try...catch
:這是處理 async/await
中可能發生錯誤的標準結構。
try { ... }
:把所有可能出錯的非同步程式碼放在這裡。
catch (error) { ... }
:如果 try
區塊中的任何一個 await
操作失敗了,程式就會立即跳到 catch
區塊來執行,並將錯誤資訊存放在 error
變數中。這確保了我們的應用程式在遇到問題時不會崩潰,而是能優雅地處理錯誤。
今天絕對是硬仗,但回報是巨大的!我們深入理解了非同步這個現代網頁開發的靈魂。
我們知道了同步會阻塞,而非同步能讓網頁保持流暢。
我們認識了 Promise,它是非同步操作的標準化「承諾」。
我們掌握了 async/await
搭配 try...catch
,這是編寫清晰、可靠的非同步程式碼的黃金組合。
為什麼我們要花一整天的時間來學習這個理論?因為 Web Bluetooth API 中的每一個函式,從掃描、連線、到讀寫資料,全都是非同步的,全都會回傳 Promise。
沒有今天的知識,我們明天將寸步難行。而現在,最後的理論障礙已經被清除。我們已經準備好迎接真正的挑戰。
明天我們將把今天這段 async/await
程式碼真正地放到我們的專案中。當你點擊「掃描」按鈕,你將親眼看到瀏覽器彈出那個你期待已久的藍牙掃描視窗。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。