Skip to main content

關於非同步 - AJAX & Promise

AJAX (非同步的 JavaScript 與 XML 技術)

AJAX 應用可以僅向伺服器傳送並取回必須的資料,並在客戶端用 JavaScript 處理來自伺服器的回應。因為在伺服器和瀏覽器之間交換的資料大量減少,伺服器回應更快了。同時,很多的處理工作可以在發出請求的客戶端機器上完成,因此 Web 伺服器的負荷也減少了。

AJAX 技術的出現,讓瀏覽器可以向 Server 請求資料而不需費時等待。當瀏覽器接收到 response 之後,新的內容就會即時地添入原本網頁

非同步是什麼?

發出 request 之後不需要等待 response ,可以持續處理其他事情,甚至繼續送出其他 request,等 response 傳回之後,就被融合進當下頁面或應用中。

當你在使用 DOM 事件時,其實你已經在運用非同步的概念,你不知道事件什麼時候會發生,但你可以先把要發生的函式準備好,等函式中的 callback 被呼叫的時候再執行要接著做的事。JavaScript 可以把函式當成值來傳遞,因此執行程序可以非同步的發生。

JavaScript 實現非同步的方法不斷演進著:從 callback 函式、promises 到最新的 async-await 函式。

!https://uploads-ssl.webflow.com/60d29cc33f302e8be91cf0e2/60d29cc33f302e40931d02e7_ExportedContentImage_02.png

  • 所有的 AJAX 都會有對應的 API 接口,API 的接口就是一段網址,不同的網址也對應不同的「HTTP 請求方法」,請求方法必須與網址完全對應才可運作。
  • AJAX 會統一由網頁端發出請求(無論新增資源或取得資源),依據不同的接口、請求方法進行請求,而伺服器會依據請求的方法、內容來進行回應。
  • AJAX 會在背景送出請求取得回應,不用等待也同時可以做其他事情,等同 Queue (佇列) 的概念,收到回應才產生一個 Queue,執行並顯示在畫面。

目前常見的技術

  • XMLHttpRequest (IE7 以上支援,jQuery,axios)

實務上很少直接使用原生的 XMLHttpRequest。取而代之的方法有很多種,過去較流行的是 jQuery 的 $.ajax(),然而在 JavaScript 日趨成熟之後,許多新的替代方案應運而生:

  • fetch (較新,HTML5 才有,IE11 以下不支援)。fetch 並不是 XMLHttpRequest 的升級版本,不是 jQuery 提供的 $.ajax 語法或 axios 那種原生 XHR 的封裝。它是一個全新的東西,並且基於 Promise 語法結構所設計,可配合使用 Async/Await 語法,使程式更加優雅。

    fetch('http://example.com/movies.json')
    .then(function(response) {
    return response.json();
    })
    .then(function(myJson) {
    console.log(myJson);
    });

    async function ajax() {
    const response = await fetch('http://example.com/movies.json');
    const data = await response.json();
    console.log(data);
    }
    ajax();
  • axios 是一個使用 Promise base 的 Ajax 函式庫,他有許多重要功能如:

    1. 廣泛的瀏覽器支持
    2. 可支援 Node.js 從後端發送的 Http request,這意味著 axios 可以兼用於前端與後端專案。
    3. 直接將回應的 JSON 資料轉換成 JavaScript 的 Object,這十分方便!

常見的非同步問題 (不限 AJAX)

  1. 回呼地獄
  2. 寫法不一致
  3. 無法同時執行 (無法確定什麼時候開始及結束)

Promise

!https://miro.medium.com/max/1400/1*gtdCpCvoZ6Q-YVC-N4en2w.png

使用 promise 可以解決 1. 回呼地獄 2. 寫法不一致 3. 無法同時執行 的問題

  • Promise 是為了解決傳統非同步語法難以建構及管理的問題。Promise 本身是一個建構函式
  • Promise 有三種狀態pending, resolved/fulfilled, rejected
  • new Promise 內的函式會立即被執行,當 resolve 得到內容後,才會執行 .then
  • 要提供一個函式 promise 功能,讓他 return 並透過 new 方式建立一個 promise 物件,並必須傳入一個函式作為參數帶有 resolve / reject 參數,分別代表成功及失敗的回傳結果。
  • 已實現的狀態會透過 resolve 這個參數回傳一個結果,在調用時使用 .then 接收回傳結果;反之否決狀態會透過 reject 參數回傳並用 .catch 接收,一次只有一種結果
  • .then 的 resolvedCallback 中,可以得到在 new Promiseresolve 內所得到的值 (value)。
  • .then() 是 promise 的 (原型) 方法
  • 如果在 .then 的 resolvedCallback 中 return 一個值,則這個值會以 Promise 物件的形式傳到下一個 .then
  • HTML5 中的 Fetch API 也是使用它
  1. 基礎運用

    const promiseSetTimeout = (status) => {
    return new Promise((resolve, reject) => { // 傳入函式參數,帶有 resolve/reject 參數
    setTimeout(() => {
    if(status) {
    resolve('promiseSetTimeout 成功')
    } else {
    reject('promiseSetTimeout 失敗')
    }
    }, 0);
    })
    }

    promiseSetTimeout(true)
    .then(function(res) { // .then 放入狀態完成的回調函式,針對狀態做處理
    console.log(res)
    })
  2. 串接 (避免回呼地獄 + 巢狀寫法)

    promiseSetTimeout(true)
    .then(function(res) {
    console.log(1, res);
    return promiseSetTimeout(true) // return 下一個 promise
    })
    .then(res => { // return 的結果會在下一個 .then 出現
    console.log(2, res)
    })

    promiseWrap(A)
    .then(() => {
    return promiseWrap(B);
    })
    .then(() => {
    return promiseWrap(C);
    })
  3. 失敗的捕捉

    除了成功的方法之外也要把失敗的方法補進去

    promiseSetTimeout(true)
    .then(res => { // 被 resolve 時執行
    console.log(res);
    })
    .catch(err => { // 被 reject 時執行
    console.log(err);
    })
  4. Promise.all

    所有的 promise 都回傳成功了才進入下一個任務,在此之前都是等待,但若其一回傳為失敗就進入失敗的處理狀況

    Promise.all([axios.get(url), axios.get(url)])
    .then([res1, res2] => {
    console.log(res1, res2)
    })
  5. 實戰運用

    const component = {
    data: {},
    init() {
    console.log(this) // this 是指向 component
    promiseSetTimeout(true)
    .then(res => { // 箭頭函式會使用外層的作用域也就是指向 component
    this.data.res = res; // 將回傳的 res 寫入 data 內
    })
    }
    }
    component.init();

    /**
    * You can get token in a Promise
    * 利用 Promise 先透過 email 和 password 取得 access_key 和 secret 後,
    * 再用 access_key 和 secret 取得 token。
    **/

    const getTokenPromise = new Promise((resolve, reject) => {
    request
    .post(endpoint + '/users/cert')
    .send({
    email: '<your_email>',
    password: '<your_password>',
    })
    .end((err, res) => {
    resolve(res);
    reject(err);
    });
    });

    getTokenPromise()
    .then((response) => {
    let parseResponse = JSON.parse(response.text);
    console.log(parseResponse);
    return new Promise((resolve, reject) => {
    request
    .post(endpoint + '/users/token')
    .send({
    access_key: parseResponse.access_key,
    secret: parseResponse.secret,
    })
    .end((err, res) => {
    resolve(res);
    reject(err);
    });
    });
    })
    .then((response) => {
    let parseResponse = JSON.parse(response.text);
    console.log(parseResponse); // You can get Token Here
    })
    .catch((err) => {
    console.warn('getTokenPromise with error', err);
    });

async...await

只要 function 標記為 async,就表示裡頭可以撰寫 await 的同步語法await 關鍵字只能在 async function 中執行,而 await 就是「等待」,它會確保一個 promise 物件都解決 (resolve) 或出錯 (reject) 後才會進行下一步,當 async function 的內容全都結束後,會返回一個 promise,這表示後方可以使用 .then 語法來做連接。

async 函式中使用 await 關鍵字意味者:「我們請 JavaScript 等待這個非同步的作業完成,才展開後續的動作,且這個函式會回傳一個 Promise 物件」,換成 Async/Await 的話,就不必寫下 .then() 了,就像同步的程式一般,不必理會它是否為非同步。

(async function() {
await promiseWrap(A);
await promiseWrap(B);
await promiseWrap(C);
}());

另外,用迴圈處理非同步事件時,需要注意 ES6 後提供的許多 Array 方法都不支援 async / await 的語法,例如使用 forEach 取代 for,結果會變成同步執行

function getFirstInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('first data')
}, 1000);
})
}

function getSecondInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('second data')
}, 2000);
})
}

// 函式前加上 async 關鍵字,告知這是一個非同步函式
async function getGroupInfo() {
// 代表等到第一筆資料回傳後,才印出結果和請求第二筆資料
const firstInfo = await getFirstInfo() // 要處理非同步的地方,呼叫函式前加上 await
console.log(firstInfo)
// 代表等到第二筆資料回傳後,才印出結果
const secondInfo = await getSecondInfo() // 要處理非同步的地方,呼叫函式前加上 await
console.log(secondInfo)
}

getGroupInfo()