瀏覽器在網頁中都是使用同一個執行緒執行 JavaScript 語法當一個功能需要花費較長時間時,非同步的寫法就…
callback function
callback function 是 Javascript 傳統的非同步處理方式
意思就是將一個 function 當成參數傳入,等取得資料或運算完成後
再呼叫 callback function處理後續事項
下面程式我們模擬從遠端伺服器取得資料,取得資料後(等待一秒後),呼叫 callback function 印出資料
function fetchData(callback) {
setTimeout(() => {
const data = 'Hello, world!';
// 在異步操作完成後呼叫回呼函式
callback(null, data); // 傳遞錯誤參數為 null,資料參數為 data
// 若要模擬錯誤,可以使用以下程式碼
// callback(new Error('Failed to fetch data'), null);
}, 1000);
}
function handleData(error, data) {
if (error) {
console.error('Error:', error);
} else {
console.log('Data:', data);
console.log('data Fetched');
}
}
console.log('Fetching data...');
fetchData(handleData);
console.log('end');
上面程式中,最終瀏覽器 console 印出來的結果順序如下
代表執行緒遇到非同步函式時,並不會等待非同步函式執行完畢,而是直接往下執行
因此,若有需要操作資料的部分,都需要寫在 callback 裡面才能確保資料已取得
Fetching data...
callback.html:25 end
callback.html:16 Data: Hello, world!
callback.html:17 data Fetched
實務上,我們也很少特地定義一個 function 來傳入,通常都是把 function 直接寫再參數內
function fetchData(callback) {
setTimeout(() => {
const data = 'Hello, world!';
// 在異步操作完成後呼叫回呼函式
callback(null, data); // 傳遞錯誤參數為 null,資料參數為 data
// 若要模擬錯誤,可以使用以下程式碼
// callback(new Error('Failed to fetch data'), null);
}, 1000);
}
function handleData(error, data) {
if (error) {
console.error('Error:', error);
} else {
console.log('Data:', data);
console.log('data Fetched');
}
}
console.log('Fetching data...');
fetchData(fetchData((error, data)=>{
if (error) {
console.error('Error:', error);
} else {
console.log('Data:', data);
}
}););
console.log('end');
以上是 callback function 簡單介紹,需注意的是若 callback function 層層堆疊時,會讓排版變的非常混亂,造成 callback hell ,這邊我就不放波動拳的梗圖了
Promise
Promise 是一個用來改善 callback function 寫法的物件,使程式碼更容易閱讀和維護
Promise 物件包含以下幾種屬性
- State:Promise 有三種狀態,分別是
pending
、fulfilled
、rejected
,剛被建立出來的時候狀態是 pending,隨後,若程式執行成功並resolve
執行結果,則狀態會變更為 fulfilled,相反的,若程式執行失敗,透過reject
回傳錯誤訊息,則狀態變更為 rejected - resolve 與 reject:Promise 可以透過 resolve 或 reject 來解析並回傳結果或錯誤訊息
- .then():當 Promise 物件使用 resolve 來解析並回傳資料時,則可以透過 .then() 來獲取回傳的資料,相當於把上面callback function的內容移到這裡面來
- .catch():當 Promise 物件使用 reject 來解析並回傳錯誤訊息時,則會進入這個區塊,用來處理錯誤維護
- .finally():無論是 resolve 或 reject 都會進入的區塊,順序在 .then() 或 .catch() 之後
下面我們就來把上面 callback function 的寫法改為 Promise 寫法
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'Hello, world!';
// 在異步操作完成後解析 Promise
resolve(data);
// 若要模擬錯誤,可以使用以下程式碼
// reject(new Error('Failed to fetch data'));
}, 1000);
});
}
console.log('Fetching data...');
const promise = fetchData();
console.log(promise)
promise
.then(data => {
console.log('Data:', data);
console.log('Data Fetched');
console.log(promise)
})
.catch(error => {
console.error('Error:', error);
console.log(promise)
})
.finally(() => {
console.log('end');
});
console.log('main thread end');
可以看到 fetchData()
改為回傳 Promise
物件
若資料取得成功,則會進入 promise.then
區塊內
而若是將第7行註解起來,第9行打開,則會進入 promise.catch
區塊內
不管是進入 .then 或是 .catch,最終都還會執行 .finally 後才結束程式
依照上面的程式,瀏覽器 print 出來的結果如下,依然可以發現程式並不會等待非同步函式執行結束後才往下走,而是先往下執行,待非同步函式完成後,才回頭去跑 .then()、.catch()、.finally()內的程式
Fetching data...
promise.html:18 Promise {<pending>}
promise.html:35 main thread end
promise.html:22 Data: Hello, world!
promise.html:23 Data Fetched
promise.html:24 Promise {<fulfilled>: 'Hello, world!'}
promise.html:31 end
而如果是將第7行註解,第9行打開,則會顯示下面訊息
Fetching data...
promise.html:18 Promise {<pending>}
promise.html:35 main thread end
promise.html:27 Error: Error: Failed to fetch data
at promise.html:9:14
(anonymous) @ promise.html:27
Promise.catch (async)
(anonymous) @ promise.html:26
promise.html:28 Promise {<rejected>: Error: Failed to fetch data
at file:///C:/Users/Vic/Desktop/promise.html:9:14}
promise.html:31 end
async/await
從 promise 又延伸出 async
與 await
的兩個新特性,其實本質上是更簡便的語法糖。
async
:宣告一個函式是非同步函式( async function ),並且這個函式所回傳的一定是 Promise 物件,非同步函式內部可以使用await
關鍵字,當遇到await
時,將暫停執行,等待 Promise 回傳結果( resolved 或 rejected ) 後,才繼續執行後續的程式碼。- await:用來等待 Promise 物件完成。可以放在非同步函式內,並且可以將 Promise 的結果賦值給一個變數
下面我們來看看將上面程式改寫為 async/await 後的寫法
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = 'Hello, world!';
// 在異步操作完成後解析 Promise
resolve(data);
// 若要模擬錯誤,可以使用以下程式碼
// reject(new Error('Failed to fetch data'));
}, 1000);
});
}
async function main() {
try {
console.log('Fetching data...');
const data = await fetchData();
console.log('Data:', data);
console.log('Data Fetched');
} catch (error) {
console.error('Error:', error);
} finally {
console.log('end');
}
}
main();
console.log('main thread end');
下面來看看這個範例,瀏覽器 print 出內容的順序為何
Fetching data...
async.html:29 main thread end
async.html:18 Data: Hello, world!
async.html:19 Data Fetched
async.html:23 end
與上面大同小異,印完 Fetching data… 後,因為函式內需暫停一秒,所以執行緒就先跳出往下執行 main thread end,待取得 17 行的結果後,才執行 18、19 行程式。
需注意的是,因為 main() 被宣告為 async,async function 一定會回傳一個 promise 物件,實務上能夠使用 promise = main()
來承接,接著在透過 .then() 之類的方式來處理後續。
以上是常見的幾種 JavaScript 處理非同步 function 的寫法,記錄下來供未來的自己參考