寫給 Java 程式設計師的前端 Promise 教程,你學會了嗎?

語言: CN / TW / HK

小夥伴們知道,松哥最近在錄 TienChin 專案,用的 RuoYi-Vue 腳手架,前端關於網路請求的地方,很多都涉及到了 Promise,為了讓小夥伴們能更好的理解前端程式碼,咱們抽空整一篇文章和大家聊聊整個 Promise。

1. 為什麼需要 Promise

假設我現在有這樣一個需求,使用者先去登入,登入成功之後,再去服務端獲取使用者資訊,獲取到使用者資訊之後再去服務端獲取前端的動態選單。一般我們的 Ajax 請求都是非同步形式,為了確保上一步操作成功再執行下一個請求,所以最終發出的請求虛擬碼類似下面這樣:

$.ajax({
    url:'/login',
    data:loginForm,
    success: function (data) {
        //登入成功
        $.ajax({
            url:'/getInfo',
            success: function (userInfo) {
                //獲取使用者資訊成功
                $.ajax({
                    url: '/getMenus',
                    success: function (menus) {
                        //獲取選單成功
                    }
                })
            }
        })
    }
})

為了確保一個非同步任務執行完成後,再執行下一個非同步任務,我們不得不在回撥函式中不停的寫下去,上面我舉的例子是三個請求巢狀,實際上可能會更多。。。這就是前端所謂的回撥地獄。而 Promise 就是來解決回撥地獄的。

2. Promise

Promise 從字面上理解,就是承諾,承諾將來在某一個時間會做某一件事。通過 Promise 我們可以將非同步任務執行的程式碼和處理的程式碼完全分離開。

還是第一小節這個例子,如果我們用 Promise 寫,那麼方式如下:

function login(resolve, reject) {
    setTimeout(() => {
        let number = Math.random();
        if (number > 0.5) {
            resolve("login success")
        } else {
            reject("login failed")
        }
    }, 2000);
}
function getInfo(resolve, reject) {
    setTimeout(() => {
        let number = Math.random();
        if (number > 0.5) {
            resolve("getInfo success")
        } else {
            reject("getInfo failed")
        }
    }, 2000);
}
function getMenus(resolve, reject) {
    setTimeout(() => {
        let number = Math.random();
        if (number > 0.5) {
            resolve("getMenus success")
        } else {
            reject("getMenus failed")
        }
    }, 2000);
}
new Promise(login).then(data => {
    console.log("login:", data);
    return new Promise(getInfo);
}).then(data => {
    console.log("getInfo:", data);
    return new Promise(getMenus);
}).then(data => {
    console.log("getMenus", data);
}).catch(err => {
    console.log("err:", err);
})

我們在 Promise 中寫非同步任務執行的程式碼,在上面的案例中,松哥通過 setTImeout 方法模擬了一個耗時操作,非同步任務執行完畢後,我們呼叫 resolve 方法返回呼叫的結果(會進入到下一步的 then 中),也可以呼叫 reject 方法表示呼叫失敗(會進入到 catch 中)。

3. then

then 中的返回值可以分為三種情況。

3.1 正常 return

then 中方法的引數,是上一個 Promise 物件 resolve 的值,一個 Promise 物件可以有多個 then,例如上面案例的登入功能,我們可以一直 then 下去:

function login(resolve, reject) {
    setTimeout(() => {
        let number = Math.random();
        if (number > 0.5) {
            resolve("login success")
        } else {
            reject("login failed")
        }
    }, 2000);
}
new Promise(login).then(data => {
    console.log("then1:", data);
    return data;
}).then(data => {
    console.log("then2:", data);
    return data;
}).then(data => {
    console.log("then3:", data);
}).catch(err => {
    console.log("err:", err);
})

這樣可以一直返回,這有點像我們 Java 中的流式程式設計。

3.2 丟擲異常

在 then 中,我們可以對返回結果進行判斷,不滿足條件也可以直接丟擲異常,這樣就會進入到最近的 catch 程式碼塊中。如下案例:

function login(resolve, reject) {
    setTimeout(() => {
        let number = Math.random();
        if (number > 0.5) {
            resolve("login success")
        } else {
            reject("login failed")
        }
    }, 2000);
}
new Promise(login).then(data => {
    console.log("then1:", data);
    throw new Error("出錯啦");
}).then(data => {
    console.log("then2:", data);
    return data;
}).then(data => {
    console.log("then3:", data);
}).catch(err => {
    console.log("err:", err);
})

像上面這段程式碼,如果進入到第一個 then 中,第一個 then 直接丟擲異常,這樣直接就進入到 catch 中了,後面的 then 就都不會執行了。

3.3 返回 Promise

第三種情況就是 then 中也可以返回一個 Promise 物件,這個就如同我們第二小節的案例,我這裡就不再贅述了。

就說一句,如果 then 中返回的是一個 Promise 物件,那麼接下來的 then 其實是這個 Promise 物件的 then,而不是一開始的 Promise 的 then 了。例如如下虛擬碼:

A.then((data)=>{return B}).then(xxx)

假設 A 和 B 都是 Promise 物件,那麼第二個 then 方法是 B 的 then。

好啦,這就是 then 中的三種返回值情況。

4. catch

catch 主要是用來處理異常的情況,兩種情況下會進入到 catch 中:

  • Promise 執行的時候通過 reject 返回資料。
  • then 中丟擲 Error

出了問題,就由最近的 catch 來處理。

5. finally

最後還有一個 finally 用來兜底,這一套下來感覺有點像我們 Java 中的 try-catch-finally,也就是前面無論如何,最終 finally 中的程式碼都會執行。不過不同於 Java 中的 finally,Promise 中的 finally 在最終執行完畢後,還可以繼續 then。。。前端的蜜汁操作。

6. 其他方法

最後,我們再來看看 Promise 中的其他靜態方法。

6.1 Promise.all()

Promise.all() 方法可以接收多個 Promise 物件,並且只返回一個 Promise 例項,這個方法會等所有輸入的 Promise 物件的 resolve 方法都返回的時候,或者所有輸入的 Promise 物件中有一個 reject 的時候,這個 all 就會執行結束,來看如下一個案例:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
    console.log(values);
}).catch(err=>{
    console.log("err", err);
})

當 promise1、promise2 以及 promise3 都執行了 resolve 的時候,就會進入到 then 中,這三個中有任意一個執行了 reject 就會進入到 catch 中。

6.2 Promise.race()

Promise.race() 方法可以接收多個 Promise 物件,一旦迭代器中的某個 Promise resolve 或 reject,返回的 Promise 就會 resolve 或 reject。

和 all 方法的區別在於,race 方法是誰執行的快,就用誰的結果。我們來看如下一段程式碼:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
    setTimeout(reject, 600, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
    console.log(value);
});

promise1 執行時間短,promise2 執行時間長,所以最終結果就是 promise1 的結果。

6.3 Promise.reject()

Promise.reject() 方法返回一個帶有 reject 原因的 Promise 物件。來看如下一段程式碼:

function resolved(result) {
    console.log('Resolved');
}
function rejected(result) {
    console.error(result);
}
Promise.reject("error").then(resolved).catch(rejected);

這個執行的時候就會進入到 catch 中。

6.4 Promise.resolve()

Promise.resolve(value) 方法返回一個以給定值解析後的 Promise 物件。

const promise1 = Promise.resolve(3);
promise1.then(data=>{
    console.log("data", data);
},err=>{
    console.log("err", err);
})

這個用法比較簡單,沒啥好說的。

還有一些其他方法我就不一一列舉了,感興趣的小夥伴們可以看看 MDN 上的相關講解:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise。

好啦,這個東西搞懂了,我們再去看 TienChin 專案的前端,就會非常容易了!