同步(Sync)& 非同步(Async) 、回呼(Callback)、Promise + then() + catch()、async + await + try + catch


筆記大綱

  1. 同步(sync)& 非同步(async)
  2. 處理非同步運算時用的回呼(callback)
  3. ES6 開始為了處理回呼地獄(callback hell)而出現的 Promise、then()、catch()
  4. ES7 出現的 Promise 語法糖:async、await、try、catch

同步(sync)& 非同步(async)

JavaScript 是一種單執行序(single-thread)語言,也就是一次只能做一件事情,例如有下列程式碼:

function showFunction(){
    console.log("This is the function.");
    }

//========================================

console.log("start");
showFunction();
console.log("end");

顯示結果如下,可發現程式碼由上到下逐一讀取:

這樣的程式碼稱為「同步(sync)」,看字面意思會以為是一次同時做很多事情,但事實上正好相反,代表的是一次做一件事情。

但若在程式碼中,不希望因為部分內容尚未執行而卡住,要在其他內容執行過程的同時,就可以執行下個階段的內容,就是「非同步(async)」,例如視窗物件的方法「setTimeout()」或「addEventListener()」等。

若使用「setTimeout()」方法,程式碼範例如下:

console.log("start");

setTimeout(() => {
    console.log("This is the code.");
}, 5000);

console.log("end");

實際執行時,會先跑「console.log(“end”);」,中間的「console.log(“This is the code.”);」要等 5 秒才會出現:

這並不代表 JavaScript 在執行程式碼時,可以同時處理「setTimeout()」與「console.log(“end”);」,而是先把「setTimeout()」的部分先丟給「Web API」處理,自己先往下跑,等「setTimeout()」設定的 5 秒鐘到了再顯示裡面的內容。


回呼(callback)

回呼的字面意思就是「等會再叫你」,回呼函式會被當成參數被放在另一個函式中,要等到適合它出場時才會發生作用。

舉例來說,現有一被放在 setTimeout() 中的程式碼如下:

function First(a){
    setTimeout(() => {
    console.log(`This is ${a}.`);
    }, 5000);
}

//========================================

console.log("start");
console.log(First("Number 1"));
console.log("end");

顯示結果如下:

程式碼剛執行時,First() 函式中的內容仍未定,因此顯示「undefined」,要到 5 秒鐘後,First() 裡面的「setTimeout()」函式才會執行。

為了避免出現上述狀況,我們將「setTimeout()」裡面的內容放進一個回呼函式,就可以等到時間 5 秒後才執行「setTimeout()」:

function First(a, callback){       //參數多了回呼函式callback
    setTimeout(() => {
        callback(`The first is ${a}.`);
    }, 5000);
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    console.log(firstSentence);
});

console.log("end");

程式碼執行時,就不會再出現「undefined」:

回呼地獄(callback hell)

如果要有第二個「setTimeout()」,則要另寫一個回呼函式如下:

function First(a, callback){
    setTimeout(() => {
    callback(`The first is ${a}.`);     //第一個回呼函式等5秒
    }, 5000);
}

function Second(b, callback){
    setTimeout(() => {
    callback(`The second is ${b}`);    //第二個回呼函式再多等3秒
    }, 3000)
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        console.log(secondSentence);
    } )
    console.log(firstSentence);
});

console.log("end");

顯示結果如下,第一個回呼函式要等 5 秒、第二個回呼函式要再等 3 秒才會執行:

如果還要有第三、第四、第五個回呼函式要執行,程式碼將會變成這樣:

function First(a, callback){
    setTimeout(() => {
    callback(`The first is ${a}.`);     //第一個回呼函式等5秒
    }, 5000);
}

function Second(b, callback){
    setTimeout(() => {
    callback(`The second is ${b}.`);    //第二個回呼函式再等3秒
    }, 3000)
}

function Third(c, callback){
    setTimeout(() => {
    callback(`The third is ${c}.`);     //第三個回呼函式再等3秒
    }, 3000)
}

function Fourth(d, callback){
    setTimeout(() => {
    callback(`The fourth is ${d}.`);    //第四個回呼函式再等3秒
    }, 3000)
}

function Fifth(e, callback){
    setTimeout(() => {
    callback(`The fifth is ${e}.`);    //第五個回呼函式再等3秒
    }, 3000)
}

//========================================

console.log("start");

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        Third("Number 3", (thirdSentence) => {
            Fourth("Number 4", (fourthSentence) => {
                Fifth("Number 5", (fifthSentence) => {
                    console.log(fifthSentence);
                })
                console.log(fourthSentence);
            })
            console.log(thirdSentence);
        })
        console.log(secondSentence);
    })
    console.log(firstSentence);
});

console.log("end");

顯示結果如下:

雖然一樣可以達到預期的效果,但是程式碼中出現了人見人怕的「回呼地獄(callback hell)」:

//回呼地獄(callback hell)

First("Number 1", (firstSentence) => {
    Second("Number 2", (secondSentence) => {
        Third("Number 3", (thirdSentence) => {
            Fourth("Number 4", (fourthSentence) => {
                Fifth("Number 5", (fifthSentence) => {
                    console.log(fifthSentence);
                })
                console.log(fourthSentence);
            })
            console.log(thirdSentence);
        })
        console.log(secondSentence);
    })
    console.log(firstSentence);
});

Promise、then()、catch()

為了解決回呼地獄的問題,ES6 起版本的 JavaScript 推出了 Promise,用來表示非同步運算最終結果為成功或失敗的物件,大幅提高程式碼的可閱讀性。

Promise 的基本架構如下:

new Promise(function(resolve, reject) => { … });

Promise 的初始狀態是「pending」,而對於用 Promise 創造出來的實例而言,函式中的「resolve」代表成功的狀況,會對應「.then()」方法;「reject」代表失敗的狀況,對應「.catch()」方法。

上述產生回呼地獄的程式碼可以用Promise改寫如下,函式中「resolve」與「reject」兩狀況以三元條件運算子(ternary operator)表示:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//=======================================================================================================

console.log("start");

First("Number 1")
    .then(() => {
        return Second("Number 2");
    })
    .then(() => {
        return Third("Number 3");
    })
    .then(() => {
        return Fourth("Number 4");
    })
    .then(() => {
        return Fifth("Number 5");
    })
    .catch((error) => {
        return error;
 })

console.log("end");

顯示結果如下,與使用回呼函式相同:

Promise 中的錯誤結果

若要回傳錯誤結果、測試「.catch()」跑到「reject」的狀況,可以在輸入的引數改為一個虛值(falsy value)或空白。

以在 Second() 函式輸入「null」範例如下:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//=======================================================================================================

console.log("start");

First("Number 1")
    .then(() => {
        return Second(null);   //這裡改成null
    })
    .then(() => {
        return Third("Number 3");
    })
    .then(() => {
        return Fourth("Number 4");
    })
    .then(() => {
        return Fifth("Number 5");
    })
    .catch((error) => {
        return error;
 })

console.log("end");

顯示結果如下:


async、await、try、catch

在 ES7 起版本的 JavaScript,出現了可閱讀性比 Promise 更高的語法糖「async / await」,可以把所有原本寫在「.then()」的內容全部放在「await」中,再把所有的「await」放進一個「async function expression」中。

為了保有確認正確或錯誤的功能,可以把所有的「await」放進「try」中、錯誤的內容放進「catch」:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

//這裡改成async function,命名為tryAsync()
async function tryAsync() {
    try {
        await First("Number 1");
        await Second("Number 2");
        await Third("Number 3");
        await Fourth("Number 4");
        await Fifth("Number 5");
    }
    catch(error) {
        return error;
    };
}

//=======================================================================================================

console.log("start");

tryAsync();

console.log("end");

顯示結果如下:

「async function expression」中的錯誤結果

現將 Second() 中的引數(argument)改為虛值(falsy value)「undefined」如下:

function First(a){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            a ? resolve(console.log(`The first is ${a}.`)) : reject(console.log(`You are wrong at 1.`));
        }, 5000);
    });
}

function Second(b){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            b ? resolve(console.log(`The second is ${b}.`)) : reject(console.log(`You are wrong at 2.`));
        }, 3000);
    });
}

function Third(c){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            c ? resolve(console.log(`The third is ${c}.`)) : reject(console.log(`You are wrong at 3.`));
        }, 3000);
    });
}

function Fourth(d){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            d ? resolve(console.log(`The fourth is ${d}.`)) : reject(console.log(`You are wrong at 4.`));
        }, 3000);
    });
}

function Fifth(e){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            e ? resolve(console.log(`The fifth is ${e}.`)) : reject(console.log(`You are wrong at 5.`));
        }, 3000);
    });
}

async function tryAsync() {
    try {
        await First("Number 1");
        await Second(undefined);      //這裡改成undefined,為一個falsy value
        await Third("Number 3");
        await Fourth("Number 4");
        await Fifth("Number 5");
    }
    catch(error) {
        return error;
    };
}

//=======================================================================================================

console.log("start");

tryAsync();

console.log("end");

顯示結果如下,一樣會因為出現錯誤而到「catch()」部分,出現 Second() 中的「reject」區塊:

#javascript #同步/非同步 #回呼 #Promise #Async/Await







你可能感興趣的文章

Join() - 將陣列內的文字連結,但是不要有逗號的方法!

Join() - 將陣列內的文字連結,但是不要有逗號的方法!

Jest - mock read-only data

Jest - mock read-only data

[JS102] npm, Jest, ES6  &[ALG101]

[JS102] npm, Jest, ES6 &[ALG101]






留言討論