一般在ajax時,幾乎沒人會用ES5以前的xmlhttp方法去處理,因為太髒又難寫、瑣碎,我們可能會使用最常見的jQuery來寫,可能會是這樣
$.getJSON('https://randomuser.me/api/',function(data){
console.log(data)
})
或者是這樣
$.ajax({
url:'https://randomuser.me/api/',
dataType:'json',
success:function(data){
console.log(data)
}
})
結果可說是一模一樣
在結構上也差不多,要給AJAX的目標對象(網址)與後續接收到資料處理的函式。
這個「函式」、「function」,就是我們所說的callback回呼函式
callback回呼函式
當我們執行非同步的呼叫時,我們不知道什麼時候執行完,
但我們可以傳入一個function給它,
當非同步執行完後,將取得的資料傳給這個function當參數,讓這個function做後續處理。
但這時有個問題,如果我們把AJAX寫在某個函式中,並且我們想取得json中的某個資料,例如這樣寫
function getEmail(){
$.getJSON('https://randomuser.me/api/',function(data){
return data.results[0].email;
})
}
const email = getEmail();
console.log(email)
宣告常數email去指向執行getEmail()的結果,我們印出常數email,結果會是如何呢?
結果會是undefined,為什麼?至少兩個原因:
1常數email接收的是getEmail( )return的結果
getEmail( )有return東西嗎?沒有!
$.getJSON內的回呼函式是將資料return給getEmail這個function,但我們並沒有在getEmail這個函式寫return東西到外面。
如果是這樣寫
function getEmail(){
$.getJSON('https://randomuser.me/api/',function(data){
return data.results[0].email;
})
return 'Hello'
}
const email = getEmail();
console.log(email)
那麼瀏覽器就會印出Hello,因為他是從getEmail這個函式return出來的。
那有些人就會說,那這樣寫可以嗎?
function getEmail(){
let mail
$.getJSON('https://randomuser.me/api/',function(data){
email = data.results[0].email;
})
return mail
}
const email = getEmail();
console.log(email)
..結果還是undefined,這又牽涉到另一個問題
2同步與非同步的順序
瀏覽器會先執行同步,才處理非同步的程式,這牽涉到Javascript事件儲列的部分,何況電腦也不知道非同步是要多久才獲得資料,
我們可以很篤定的說,getEmail()的執行順序是:
1.let宣告變數mail
2.發出getJSON請求
3.getEmail回傳變數mail給外層的變數email
4.console.log印出變數email
所以console.logh才印出undefined,因為變數預設值是undefined,但其實還有個動作
5.瀏覽器過了幾毫秒後取的json的值,並回傳裡頭的data.results[0].email;給getEmail
但5毫無意義,因為getEmail()除了$.getJSON外都是同步的,這時程式也不會有什麼動作
會造成這些問題,最主要是因為我們把非同步呼叫和同步程式混再一起寫
這情況要怎麼解呢?
不該用會做後續使用的同步變數、常數去指向非同步程式回傳值
我們應該傳入一個callback給getEmail( ),讓這個callback去做後續的處理
將程式碼改成這樣就好
function getEmail(callback){
$.getJSON('https://randomuser.me/api/',function(data){
callback(data.results[0].email)
})
}
// 呼叫執行getEmail()時傳入一個function
getEmail(function(mail){
console.log(mail)
});
getEmail的宣告式,多加了函式當參數傳入,並且取得資料時,呼叫這個函式做其他處理
callback回呼函式的問題
callback雖然解決了同步、非同步呼叫的順序問題,但它並不是萬能的,在應用上還是有些問題狀況。
承接上面程式碼去修改,假設我們要「按順序連續取得3個mail」
取得第一個mail時,把第一個mail推進陣列,接著才取第二個;
取得第二個mail時,把第二個mail推進陣列,接著才取第三個;
取得第三個mail時,把第三個mail推進陣列,接著才呼叫後續處理陣列的函式...那麼...
...程式碼會變這樣
// 宣告一個陣列
const mailArray = [];
function getEmail(callback) {
$.getJSON("https://randomuser.me/api/", function(data) {
callback(data.results[0].email);
});
}
// 呼叫執行getEmail()時傳入一個function
getEmail(function(mail) {
// 把第一個mail推進陣列
mailArray.push(mail);
console.log(mail);
// 在執行一次 取得第二個mail
getEmail(function(mail) {
// 把第二個mail推進陣列
mailArray.push(mail);
console.log(mail);
// 在執行一次 取得第三個mail
getEmail(function(mail) {
// 把第三個mail推進陣列
mailArray.push(mail);
console.log(mail);
});
});
});
這...太巢狀了吧,當我們要讓非同步的AJAX程式,按照callback先後順序來呼叫,就會發生這種狀況
目前只有三個還好,一但數量更多,就會變成所謂回呼地獄(callback hell)
這樣子程式一個包一個,辨識度實在不好,有沒有什麼方法可以解決這問題呢?
ES6的promise
promise是一個物件,這個物件有很多狀態,包含執行中、執行完成、執行失敗...etc,
每個promise動作,我們可以用.then,把程式碼連結起來。
我們將getEmail改成這樣:用new建立一個Promise物件並回傳,在傳入Promise的函式去非同步呼叫
// 宣告一個陣列
const mailArray = [];
function getEmail() {
// 用new建構子建立Promise物件
// resole 成功時做什麼動作、reject 失敗時做什麼動作
return new Promise(function(resole, reject) {
$.getJSON("https://randomuser.me/api/", function(data) {
resole(data.results[0].email);
});
});
}
接著我們可以把剛剛的回呼地獄程式用then改成這樣
getEmail()
.then(function(mail){
// 把第一個mail推進陣列
mailArray.push(mail);
console.log(mail);
// 在執行一次 取得第二個mail
return getEmail();
})
.then(function(mail){
// 把第二個mail推進陣列
mailArray.push(mail);
console.log(mail);
// 在執行一次 取得第三個mail
return getEmail();
})
.then(function(mail){
// 把第三個mail推進陣列
mailArray.push(mail);
console.log(mail);
console.log(mailArray);
})
顯示的結果會和回呼地獄的範例一樣。
雖然還是有callback回呼函式的影子,但已經直覺很多了,
一個動作做完做下一個,所有的then都是串在一起,而不是一層一層往程式碼內去衍生包覆,這樣一來可讀性高上了不少。
ES7的async、await
async、await是ES7以後新增的語法,目的是讓非同步的程式碼,看上去更像JS同步程式的非同步語法。
如果說promise是基於callback去衍生進步的非同步寫法,
那麼await、async就是基於promise更衍生進步的非同步寫法。
字面上來看,await: 等待、async: 非同步。下面程式碼,我們不改getEmail()promise的寫法,只改從原本回呼地獄,改成then的程式碼
// async、await寫法
async function showMail(){
//宣告mail變數
let mail;
//mail變數指向getEmail執行的結果,也就是Promise非同步AJAX的結果
mail = await getEmail()
// 把第一個mail推進陣列
mailArray.push(mail);
console.log(mail);
mail = await getEmail()
// 把第二個mail推進陣列
mailArray.push(mail);
console.log(mail);
mail = await getEmail()
// 把第三個mail推進陣列
mailArray.push(mail);
console.log(mail);
console.log(mailArray)
}
// 呼叫執行這個async函式
showMail()
雖然可以落單,但基本上async、await是成雙出現的
async寫在function 名字()之前
await寫在要執行promise的函式之前
如果我們的非同步是用promise去撰寫的,我們可以用async函式去awiat它
這種寫法可以讓非同步的程式看起來像是同步的程式一樣
這邊會發現,和callback不一樣,明明用一個變數去指向非同步的程式,卻不會印出undefined?
這是因為await會確保AJAX到東西後,才執行後續的動作,所以這句
mail = await getEmail()
await會把getEmail( )這個非同步promise執行完,把值賦予給mail變數後,才會執行下一行的程式
mailArray.push(mail);
然後才執行下面的
console.log(mail);
乍看會像是阻塞了程式碼順序,又有點和.then很像,只是把.then特性改成await。
await要完成這個promise裡的動作(成功、失敗),才接續執行程式,但async、await並不是真的阻塞程式碼,因為他本質上仍然是非同步程式。
利用這種簡潔寫法,像上面的範例還可以直接改成這樣
// async、await寫法
async function showMail(){
//宣告mail變數
let mail;
// 把第一個mail推進陣列
mailArray.push(await getEmail());
// 把第二個mail推進陣列
mailArray.push(await getEmail());
// 把第三個mail推進陣列
mailArray.push(await getEmail());
console.log(mailArray);
}
// 呼叫執行這個async函式
showMail()
這樣結果一樣
陣列依序保存三個mail資料,而且是按照非同步依序push進去,然而程式卻更簡潔。
唯一要注意的是async、await是ES7+新增的語法(有人說是ES7、有人說是ES8)
這是因為async、await還在草稿階段,跟ES6已經正式發表的狀況不一樣,
只不過,前端仍然可以透過BABEL轉譯器轉意成ES5語法,優先體會到async、await強悍的力量。