一般在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強悍的力量。