BACK
Featured image of post 【Callback、Promise、Async/Await】處理非同步事件

【Callback、Promise、Async/Await】處理非同步事件

Javascript 的主要特點有兩個:Single Thread (單線程)、Synchronous (同步)。

參考彭彭 - 回呼函式 Callbacks、Promises 物件、Async/Await 非同步流程控制

前言

Javascript 的主要特點有兩個:

  • Single Thread (單線程)
  • Synchronous (同步)

當主程式中遇到「非同步」的函式如排程取資料讀檔…等相關的函式時(如:setTimeoutfetch…等),主程式並不會停止並等待函式執行完成後才繼續往下跑,而是會將其放到 queue 中直到所有程式碼都跑完了,Javascript 會再回頭到 queue 中按順序將 function 拉回來處理。

1
2
3
4
5
console.log("程式開始");
setTimeout(() => {
    console.log("非同步事件");
}, 0)
console.log("程式結束");

程式開始
程式結束
非同步事件 <- 最後執行

上段的原始碼中,setTimeout 所定義的時間為 0,但因為是屬於非同步事件,因此還是會在其他原始碼運行完以後才執行,在 Ajax 的行為中也是一樣,當需要確保擷取到遠端資料才繼續往下執行時,如果程式碼是依序撰寫的方式,就會無法正確呈現資料,以下舉個例子示範。


舉例

以下為例,此為一個很基本的回傳n1+n2的結果的函式。

1
2
3
4
5
6
7
8
9
function add(n1, n2) {
    return n1+n2;
}

function test() {
    let result = add(3, 4);
    console.log(result)
}
test();
1
> 7

但若今天需求需要延遲兩秒再將結果打印出來時。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function delayedAdd(n1, n2, delayTime) {
    window.setTimeout(function() {
        return n1+n2;
    }, delayTime);
}

function test() {
    let result = delayedAdd(3, 4, 2000);
    console.log(result);
}
test();
1
> undefined

此時會發現打印出來的 result 因為 delayedAdd 還未將結果回傳,導致直接顯示 undefined,此時就需要來解決這種非同步流程的控制。


方法1:Callbacks 回呼函式

  • 最早期使用的方式,於函式最後設定callback,執行需要做的事情。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Callback 回呼函式
function delayedAdd(n1, n2, delayTime, callback) {
    // 設定排程,延遲一段時間後執行
    window.setTimeout(function() {
        // 延遲一段時間之後,計算加法,呼叫 callback 函式
        callback(n1+n2);
    }, delayTime);
}

function test() {
    delayedAdd(3, 4, 2000, function(result) {
        console.log(result);
    });
}
test();
1
> 7

方法2:Promise 物件

  • 近期於ES6提出的新方式。
  • 建立 promise 物件:new Promise(執行函式)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    let p = new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 呼叫resolve將結果回傳
        }, delayTime);
    });
    return p;
}

function test() {
    let promise = delayedAdd(3, 4, 2000);
    promise.then(function(result){
        console.log(result);
    });
}
test();
1
> 7
常見寫法,直接 return promise
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 呼叫resolve將結果回傳
        }, delayTime);
    });
}

function test() {
    let promise = delayedAdd(3, 4, 2000);
    promise.then(function(result){
        console.log(result);
    });
}
test();
1
> 7
  • new Promise(resolve, reject) 的 resolvereject 為 Promise 原生提供,可自行改名但順序不可異動。
  • resolve 對應.then()且代表成功,而 reject 則對應到 .catch()且代表失敗。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            reject(n1+n2);    // 若使用reject,則需使用.catch
        }, delayTime);
    });
}

function test() {
    let promise = delayedAdd(3, 4, 2000);
    // 因使用 reject 回傳,這裡使用 .catch 來接
    promise.catch(function(error){
        console.log(error);
    });
}
test();
1
> 7

方法3:Async/AwaitPromise 的語法糖

  • Promise的語法糖,以便於更直觀的閱讀與撰寫非同步流程,同 Promise 於ES6提出的新玩意兒。
  • 注意:await 的函式必須要確保有Promise,否則將報錯。
  • 需先宣告為 async 函式,才能使用 await
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 若使用reject,則需使用.catch
        }, delayTime);
    });
}
// 需先宣告為 async 函式,才可使用 await
async function test() {
    let result = await delayedAdd(3, 4, 2000);
    console.log(result);
}
test();
1
> 7
  • 注意:使用 await 時,主程式會確實停止,等待 await 函式執行完成後,才接續往下跑,舉例:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 若使用reject,則需使用.catch
        }, delayTime);
    });
}
// 需先宣告為 async 函式,才可使用 await
async function test() {
    let result = await delayedAdd(3, 4, 2000);
    console.log(result);
    console.log("Hello");
}
test();
1
2
> 7
> Hello

延伸一:多個 Promise 的資料處理:Promise.all()

  • 若遇到多個 Promise,且需將各個 Promise 的結果進行運算時,可使用 Promise.all() 來處理。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 呼叫resolve將結果回傳
        }, delayTime);
    });
}

function test() {
    let promise1 = delayedAdd(3, 4, 2000);
    let promise2 = delayedAdd(2, 3, 3000);
    // 多個 Promise 都完成之後,將其數值相乘
    Promise.all([promise1, promise2]).then(function(results){
        // 此時 results 為一個陣列 > [7, 5];
        let answer = results.reduce(function(total, value) {
            return total * value;
        })
        console.log(answer);
    });
}
test();
1
> 35

延伸二:多個 Promise 使用 Async/Await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function delayedAdd(n1, n2, delayTime) {
    // 建立 Promise,使用resolve或reject回傳
    return new Promise(function(resolve, reject) {
        window.setTimeout(function() {
            resolve(n1+n2);    // 呼叫resolve將結果回傳
        }, delayTime);
    });
}

function test() {
    let result1 = await delayedAdd(3, 4, 2000);
    let result2 = await delayedAdd(2, 3, 3000);
    let answer = result1 * result2;
    console.log(answer);
}
test();
1
> 35

comments powered by Disqus