iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1
Modern Web

JavaScript基本功修煉系列 第 25

JavaScript基本功修練:Day25 - Promise

今天會介紹常用來處理非同步程式的語法:Promise。在ES6的Promise語法出現之前,我們都依賴回傳函式來寫非同步程式,但Promise的出現,令整個非同步程式的流程結構更易讀更易維護。

本文會解釋:

  • 為什麼需要Promise?
  • Promise基本語法
  • Promise鏈接
  • Promise常用方法(例如Promise.allPromise.race)
  • Promise改寫XMLhttprequest

為什麼需要Promise?

Promise與回傳函式一樣,同樣是用來處理非同步程式。最常用到非同步程式的情況,都是跟網絡連線有關(例如fetch抓取遠端資料),或者跟排程有關(例如setTimeout)。

不同的是,Promise改善了非同步的語法結構,比起只用回傳函式,更易閱讀和維護。過往我們要確保非同步函式完成後才執行某個函式,我們會用到回傳函式的方法來完成,可是如果在該回傳函式裏再接一個回傳函式,形成多層的巢狀結構,就會造成回傳地獄(callback hell)的慘況。但用Promise去寫,就會變得較易閱讀。

Promise出現的是async/await,也是目前最新的寫法,它是把Promise寫得更簡潔的語法糖,背後操作原理與Promise是一樣。總括來說,Callback function、Promise、async/await其實都是在解決同一個問題:確保非同步函式完成後才執行某個函式

Promise基本語法

Promise用處:
我們可以透過Promise物件,得出一個非同步函式在完成後,最後得出的成功或失敗結果值。

Promise是一個建構函式,我們要用new來產生一個實體的Promise物件:

//以下只用作解說Promise基本語法,很少會這樣用Promise

const p = new Promise(function(resolve,reject){ //傳入函式,帶有2個参數
    //你想執行的非同步工作
    setTimeout(function(){
        resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
    },1000)
})

在建立Promise物件時,需要傳入一個函式作為参數,該函式裏須帶有兩個参數,分別是resolvereject函式。

resolve函式:

  • 會將Promise的狀態由pending變成fulfilled
  • 把成功的結果當作参數傳出去

reject函式:

  • 會將Promise的狀態由pending變成rejected
  • 把失敗的結果當作参數傳出去

取得Promise的結果(then/catch)

以上例子是假設Promise所處理的工作結果是成功。但只是寫以上步驟的話,是不會抽取到成功的結果(即是resolve(...)裏的東西)。我們之後還需要在該Promise物件中使用then方法,把函式resolve中的参數(即是成功的結果)抽取出來:

const p = new Promise(function(resolve,reject){
    //你想執行的非同步工作
    setTimeout(function(){
        resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
    },1000)
})

//在Promise物件使用then方法
p.then(function(result){
    //result 是任何你由上方 resolve(...) 傳入的東西
    console.log(result) //成功!
})

失敗的結果,就可以用catch方法來取得:

const p = new Promise(function(resolve,reject){
    setTimeout(function(){
        reject('失敗!');
    },1000)
})

p.then(function(result){
    console.log(result); 
})
.catch(function(error){
    console.log(error); //失敗!
})

注意:resulterror這兩個名字是任你改的。同樣地,在Promise物件裏的resolvereject這兩個名字都是任你改,只是我們習慣會用resolvereject

then也可以抽取失敗結果,但不建議

then方法除了可以抽取成功結果,也能抽取失敗結果。我們可以在then方法同時放兩個回傳函式作為参數:

p.then(function(result){
    console.log(result); 
},function(error){
    console.log(error); //失敗!
})

然而,在捕捉錯誤時,用catch寫法比then更好,因為在除錯時,我們可以集中在catch之前的then裏面尋找問題。同時也比較接近以前會用的try/catch寫法。

// 不建議用then寫法
p.then(function(result) {
    // success
  }, function(error) {
    // error
  });

// 建議用catch寫法
p.then(function(result) { 
    // success
  })
  .catch(function(error) {
    // error
  });

Promise物件的狀態

上面有輕輕提及過Promise可以有3種狀態,包括:pendingfulfilledrejected

  • pending:事件運作中,還未有結果
  • resolved:事件已完成,結果成功,回傳resolve(...)裏的結果
  • rejected:事件已完成,結果失敗,回傳rejected(...)裏的結果

Promise要麼回傳resolved,要麼回傳rejected,而且只會回傳一次

例如我把以上例子改回成功結果,並且在不同位置查看一下Promise的狀態:

const p = new Promise(function(resolve,reject){
    setTimeout(function(){
        resolve('成功!'); //呼叫resolve函式,會把Promoise狀態變成fulfilled
    },1000)
})

p.then(function(result){
    console.log(p) //Promise {<fulfilled>: "成功!"}
    console.log(result) 
})

//p是非同步,所以下面這行會先被執行,這時候p是pending狀態
console.log(p) //Promise {<pending>} 

以上例子就是Promise執行完setTimeout函式後得出成功結果後,從pending狀態變成fulfilled狀態的例子。

為什麼最後一行會得出pending,之後上面才得出fulfilled? 因為setTimeout的程式是非同步的,主程式會先跳過它,直接執行最後一行console.log(p),這時候setTimeOut裏的工作並未被執行,所以Promise就是pending狀態。當setTimeout的計時結束,主程式回去執行setTimeOut裏的程式,並且結果是成功,所以Promise就會由pending變成fulfilled

下圖具體解釋了Promise狀態的變化流程:

圖片來源:JAVASCRIPT.INFO

Promise鏈接

我們需要完成一個非同步函式後,接著執行下一個非同步函式。這時候我們就可以用鏈接的技巧。以下例子是把兩個數字相加,如果結果是整數就會回傳resolve,負數就會回傳reject。例子参考自這裏

function add(a,b){
    return new Promise( (resolve,reject) => {
        window.setTimeout( () => {
            let sum = a + b;
            sum > 0 ? resolve(`${sum}, 成功`) : reject(`${sum}, 失敗`);
        },1000);
    })
}

add(10,10)
    .then( success => {
        console.log(success); //20, 成功
        return add(5,10);
    })
    .then( success => {
        console.log(success); //15, 成功
        return add(3,-5);
    })
    .then( success => {
        console.log(success); 
        return add(0,2) //沒有被執行
    })
    .catch( error => {
        console.log(error); //-2, 失敗
    })

當其中一個結果(add(3,-5))是失敗時,就會跳過下一個then方法,直接跳到catch方法。所以在以上例子中,add(0,2)是不會被執行。所以要注意,這裏全部4個Promise物件(add(10,10)、add(5,10)、add(3,-5)、add(0,2)),只要其中一個的結果是錯誤,就會跳到catch方法。

不止限於Promise函式,也可以回傳其他表達式

thencatch這些抽取成功或失敗結果值的方法裏,我們不止可以再回傳Promise物件,也可回傳其他表達式。以下範例中,執行完check(2)後,如果check(2)的結果成功,我就回傳另一個函式multiply

function check(num1){
    return new Promise( (resolve,reject) => {
        window.setTimeout( () => {
            num1 > 0 ? resolve(num1) : reject(`${num1},失敗`)
        },1000);
    })
}

function multiply(num2){
    console.log(`${num2} * 10 = ${num2*10}`) //2 * 10 = 20
}

check(1)
    .then( success => {
        console.log(success); //1
        return check(2)
    })
    .then( success => {
        //回傳其他表達式
        return multiply(success);
    })
    .catch( error => {
        console.log(error);
    })

Promise常用方法

Promise常用方法除了thencatch,還有:

  • Promise.all
    以陣列形式傳入多個Promise函式,並完成所有Promise函式,最後把所有結果集合在一個陣列裏,並且回傳
  • Promise.race
    以陣列形式傳入多個Promise函式,回傳最快完成的那個Promise物件
  • Promise.rejectPromise.resolve
    直接定義Promise物件的狀態是reject或者resolve

Promise.all

以陣列形式傳入多個Promise函式,一定會等到完成所有Promise函式,才會回傳結果,而結果就是一個放有所有Promise物的陣列。例如以下例子,我需要p1p2都一併完成,才會執行下一個動作:

function add(num1,num2,delayTime){
    return new Promise( (resolve,reject) => {
        window.setTimeout( () => {
            resolve(num1 + num2);
        },delayTime)
    })
}

const p1 = add(10,20,1000);
const p2 = add(60,70,3000);

Promise.all([p1,p2]).then( success => {
    let [result1,result2] = success
    console.log(result1,result2) //30,130
})

Promise.race

以陣列形式傳入多個Promise函式,回傳最快完成的那個Promise物件。

Promise.race([p1,p2]).then( success => {
    console.log(success) //30
})

Promise.rejectPromise.resolve

直接定義Promise物件的狀態是reject或者resolve。範例如下:

const p1 = Promise.resolve('Hello World');
const p2 = Promise.reject('Error!');

Promise.all([p1,p2])
    .then( success => {
        console.log(success);
    })
    .catch( error => {
        console.log(error); //Error! 
    })

Promise改寫XMLHttpRequest來處理AJAX

重溫我們如何用XMLHttpRequest來抓取遠端伺服器的資料:

const xhr = new XMLHttpRequest();
xhr.open('get','https://randomuser.me/api/',true);
xhr.send(null);

//當遠端資料已傳回來,就執行以下函式
xhr.onload = () => {
    if(xhr.status === 200){
        console.log(xhr.responseText); 
    }else{
        console.log('請求失敗!')
    }
}

以上範例以XMLHttpRequest建構函式來產生XMLHttpRequest實體物件,並向遠端發出資料請求。

如果用Promise重寫的話,做法就是把XMLHttpRequest的部分放在Promise裏處理:

function getJSON(url){
    return new Promise( (resolve,reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('get',url);
        xhr.send(null);

        xhr.onload = () => {
            if(xhr.status === 200){
                resolve(JSON.parse(xhr.responseText));
            }else{
                reject(new Error(xhr.statusText));
            }
        }
    });
};

//把url當作参數
getJSON('https://randomuser.me/api/')
    .then(JSONdata => console.log(JSONdata)) 
    .catch(error => console.log(error))

這個做法提高了程式碼的可讀性。如果我們之後又要從不同遠端伺服器抓取資料時,就可以重用getJSON的函式,直接把url當作参數傳進函式裏就行。

参考資料

你懂 JavaScript 嗎?#24 Promise
Promise 对象
JavaScript Promise 全介紹


上一篇
JavaScript基本功修練:Day24 - 非同步執行與事件佇列
下一篇
JavaScript基本功修練:Day26 - Promise的語法糖:async/await
系列文
JavaScript基本功修煉31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
WILL.I.AM
iT邦新手 3 級 ‧ 2022-10-08 12:34:58

有筆誤, 在 「Promise物件的狀態 」 的小節裡:
「resolved:事件已完成,結果成功 略... 」, 應該不是 resolved 而是fulfilled

我要留言

立即登入留言