昨天,我們正面迎擊了 JavaScript 中最核心的挑戰——非同步程式設計。透過「點咖啡」的生動比喻,我們理解了 Promise 如同一張「取餐券」,並學會了使用 async/await
這一現代語法,來優雅地等待我們的「咖啡」準備好。
但是,與藍牙裝置的完整互動,遠比點一杯咖啡要複雜。它更像是一場「尋寶任務」,需要一步一步地解開謎題,每一步都依賴於上一步的結果,而且每一步都需要時間。
這個尋寶流程是這樣的:
首先,你需要在茫茫大海中找到藏寶圖所在的島嶼(掃描
到裝置)。
然後,你需要登陸島嶼並建立一個營地(連接
到裝置)。
然後,你需要根據藏寶圖的指示,找到島上那個藏著寶藏的山洞(取得
服務)。
最後,你才能在山洞裡拿到那個寶箱(取得
特徵)。
這四個步驟環環相扣,而且每一個都是耗時的非同步操作。你不可能在找到島嶼之前就去尋找山洞。這種「一個非同步操作接著下一個非同步操作」的場景,就是我們今天要精通的核心——Promise Chaining (承諾的串連)。
今天,我們將深入探索如何將多個 Promise 像鎖鏈一樣串連起來,以完成複雜的連續任務。我們將學習經典的 .then()
鏈式呼叫,並再次見證 async/await
如何將這個複雜的過程,簡化得如詩一般優雅。這是我們在真正動手連接藍牙裝置前的最後一塊、也是最重要的一塊拼圖。
學習目標:
理解依賴性:明白為什麼與 BLE 裝置互動本質上是一系列有依賴關係的非同步步驟。
學習 .then()
鏈:掌握使用 .then()
來串連多個 Promise 的經典方法。
精通 async/await
序列:將鏈式呼叫的邏輯,用 async/await
寫出更清晰、更易維護的程式碼。
.then()
的鏈式呼叫在 async/await
普及之前,開發者們使用 .then()
來串連 Promise。它的核心規則是:
如果在
.then()
的回呼函式中return
了一個新的 Promise,那麼下一個.then()
就會等待這個新的 Promise 完成,並接收其結果。
讓我們用模擬的函式來演示這場「尋寶任務」。
假設我們有三個函式,每個都會回傳一個會在 1 秒後完成的 Promise:
// 模擬掃描裝置
function fakeScan() {
console.log('Step 1: Scanning for the island...');
return new Promise(resolve => {
setTimeout(() => resolve({ id: 'Island-A', name: 'Treasure Island' }), 1000);
});
}
// 模擬連接裝置
function fakeConnect(device) {
console.log(`Step 2: Connecting to ${device.name}...`);
return new Promise(resolve => {
setTimeout(() => resolve({ id: 'Server-A', deviceName: device.name }), 1000);
});
}
// 模擬獲取服務
function fakeGetService(server) {
console.log(`Step 3: Getting service from server ${server.id}...`);
return new Promise(resolve => {
setTimeout(() => resolve({ uuid: '0000180d-...' }), 1000);
});
}
.then()
串連任務console.log('--- Starting the treasure hunt with .then() chain ---');
fakeScan()
.then(device => {
// 第一個 .then 接收到 fakeScan 的結果 (device 物件)
console.log(`Success! Found: ${device.name}`);
// **關鍵:回傳下一個 Promise,將結果傳遞下去**
return fakeConnect(device);
})
.then(server => {
// 第二個 .then 接收到 fakeConnect 的結果 (server 物件)
console.log(`Success! Connected to server: ${server.id}`);
// **再次回傳下一個 Promise**
return fakeGetService(server);
})
.then(service => {
// 第三個 .then 接收到 fakeGetService 的結果 (service 物件)
console.log(`Success! Got service with UUID: ${service.uuid}`);
console.log('--- Treasure hunt complete! ---');
})
.catch(error => {
// **優點:鏈中的任何一個環節出錯,都會直接跳到這個 catch**
console.error('Oh no! The hunt failed:', error);
});
打開開發者工具的 Console 執行這段程式碼,你會看到每隔一秒,就會有一條成功的日誌印出。.then()
鏈就像一節節的火車車廂,順利地將上一步的結果,載送到下一步。
這種寫法雖然可行,但當步驟一多,一層層的 .then()
會讓程式碼看起來像一個「>」符號,可讀性會變差。
async/await
的同步化寫法現在,讓我們見證奇蹟。我們將使用 async/await
來完成完全相同的任務。
async/await
串連任務// 我們必須把所有 await 操作都放在一個 async 函式中
async function startTreasureHunt() {
console.log('--- Starting the treasure hunt with async/await ---');
try {
// Step 1: 等待掃描完成,並將結果存入 device 變數
const device = await fakeScan();
console.log(`Success! Found: ${device.name}`);
// Step 2: 使用上一步的 device,等待連接完成
const server = await fakeConnect(device);
console.log(`Success! Connected to server: ${server.id}`);
// Step 3: 使用上一步的 server,等待獲取服務完成
const service = await fakeGetService(server);
console.log(`Success! Got service with UUID: ${service.uuid}`);
console.log('--- Treasure hunt complete! ---');
} catch (error) {
// 同樣地,任何一個 await 失敗,都會直接跳到 catch
console.error('Oh no! The hunt failed:', error);
}
}
// 執行我們的 async 函式
startTreasureHunt();
請比較上面兩段程式碼。你會發現:
可讀性:async/await
版本的程式碼,讀起來就像普通的同步程式碼,完全符合人類的直覺思考順序:做完第一步,再做第二步...
變數管理:在 async/await
中,device
, server
, service
這些變數都在同一個 try
區塊中,可以自由使用。但在 .then()
鏈中,每個變數都只活在自己的那個小小的回呼函式裡。
錯誤處理:try...catch
的語法是 JavaScript 中處理錯誤的標準結構,比 .catch()
更為通用和直觀。
async/await
並沒有發明新東西,它只是 Promise 的一層「語法糖」,讓我們能用更優雅、更簡單的方式來編寫和閱讀非同步的串連邏輯。這正是我們將在專案中使用的風格。
今天,我們補上了非同步程式設計的最後一塊理論拼圖
至此,我們已經對非同步操作的「是什麼」(Promise
)、「如何處理」(.then
, async/await
) 以及「如何串連」(Chaining
) 有了全面的認識。
我們將利用接下來的兩天,放慢腳步,將 async
和 await
這兩個威力強大的語法徹底內化。
明天我們將會重新聚焦並深入探討 async
關鍵字本身。我們將會拋開複雜的藍牙連線場景,透過更多元、更基礎的範例,來徹底理解一個函式一旦被標記為 async
之後,它到底發生了什麼本質上的變化?它回傳的到底是什麼?以及我們該如何使用它。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。