筆記大綱
- 同步(sync)& 非同步(async)
- 處理非同步運算時用的回呼(callback)
- ES6 開始為了處理回呼地獄(callback hell)而出現的 Promise、then()、catch()
- 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」區塊: