了解 JavaScript 承诺

介紹

JavaScript 的 Promise 可能難以理解。因此,我想記錄一下我對 Promise 的理解方式。

理解 Promise

簡而言之,Promise 是:

“想像你是個孩子。你媽媽承諾下週給你買一個新手機。”

在下周之前,你不知道是否會得到那部手機。你媽媽可能真的會買一部新手機給你,或者她不會

這就是一個承諾。一個承諾有三種狀態。它們是:

  1. Pending:你不知道是否會得到那部手機
  2. Fulfilled:媽媽很高興,她買了一部新手機給你
  3. Rejected:媽媽不開心,她沒有買手機給你

創建一個 Promise

讓我們將這個轉換成 JavaScript。

// ES5: 第一部分

var isMomHappy = false;

// 承諾
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // 已完成
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // 拒絕
        }

    }
);

這段代碼本身就相當表達清楚。

以下是一般承諾語法的樣子:

// 承諾語法看起來像這樣
new Promise(function (resolve, reject) { ... } );

使用承諾

現在我們有了承諾,讓我們來使用它:

// ES5: 第二部分

var willIGetNewPhone = ... // 從第一部分繼續

// 調用我們的承諾
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // 耶,你得到了一部新手機
            console.log(fulfilled);
             // 輸出:{ brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // 哎呀,媽媽沒買
            console.log(error.message);
             // 輸出:'媽媽不開心'
        });
};

askMom();

讓我們運行這個例子並看看結果!

演示:https://jsbin.com/nifocu/1/edit?js,console

串接 Promise

Promise 是可以串接的。

假設你,這個孩子,承諾你的朋友,當你媽媽給你買了一部新手機後,你會展示給他們看

這是另一個承諾。讓我們寫下來!

// ES5

// 第二個承諾
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

備註:我們可以將上述程式碼簡化如下:

// 簡化它

// 第二個承諾
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

讓我們串接這些承諾。你,這個孩子,只能在willIGetNewPhone承諾完成後,開始showOff承諾。

// 呼叫我們的承諾
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // 在這裡串接
    .then(function (fulfilled) {
            console.log(fulfilled);
         // 輸出:'嘿,朋友,我有了一部新的黑色三星手機。'
        })
        .catch(function (error) {
            // 哎呀,媽媽沒有買
            console.log(error.message);
         // 輸出:'媽媽不開心'
        });
};

這就是如何串接承諾的方式。

承諾是異步的

承諾是異步的。讓我們在調用承諾之前和之後記錄一條消息。

// 調用我們的承諾
var askMom = function () {
    console.log('before asking Mom'); // 記錄之前
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // 記錄之後
}

預期的輸出序列是什麼?你可能會預期:

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

然而,實際的輸出序列是:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

你不會在等待你媽媽的承諾(新手機)時停止玩耍。這就是我們所稱的異步:代碼將在不阻塞或等待結果的情況下運行。任何需要等待承諾才能繼續的代碼都放在.then中。

以下是完整的 ES5 示例:

// ES5: 完整範例

var isMomHappy = true;

// 承諾
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // 已履行
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // 拒絕
        }

    }
);

// 第二個承諾
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

// 呼叫我們的承諾
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // 在此鏈接
    .then(function (fulfilled) {
            console.log(fulfilled);
            // 輸出: '嘿朋友,我有了一部新的黑色三星手機。'
        })
        .catch(function (error) {
            // 哎呀,媽媽沒買
            console.log(error.message);
            // 輸出: '媽媽不開心'
        });
};

askMom();

ES5, ES6/2015, ES7/Next 中的承諾

ES5 – 大多數瀏覽器

如果包含 Bluebird 承諾庫,演示代碼在 ES5 環境(所有主流瀏覽器 + NodeJs)中是可行的。這是因為 ES5 本身不支持承諾。另一個著名的承諾庫是 Kris Kowal 的 Q

ES6 / ES2015 – 現代瀏覽器, NodeJs v6

演示代码开箱即用,因为ES6原生支持Promise。此外,借助ES6的功能,我们可以用箭头函数进一步简化代码,并使用constlet

以下是完整的ES6代码示例:

//_ ES6:完整示例_

const isMomHappy = true;

// Promise
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // 箭头函数
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 第二个Promise
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// 调用我们的Promise
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // 箭头函数
        .catch(error => console.log(error.message)); // 箭头函数
};

askMom();

请注意,所有的var都替换为了const。所有的function(resolve, reject)都简化为(resolve, reject) =>。这些改动带来了一些好处。

ES7 – Async/Await

ES7引入了asyncawait语法。它使得异步语法更易于理解,无需使用.then.catch

用ES7语法重写我们的示例:

// ES7:完整示例
const isMomHappy = true;

// Promise
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 第二个Promise
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// 以ES7的async await风格调用我们的Promise
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// 这里也使用async await
(async () => {
    await askMom();
})();

承諾及其使用時機

為何我們需要承諾?在承諾出現之前,世界是怎樣的?在回答這些問題之前,讓我們回歸基礎。

普通函數與非同步函數

讓我們看看這兩個例子。兩個例子都執行了兩個數字的加法:一個使用普通函數進行加法,另一個則是遠程加法。

普通函數進行兩數相加

// 正常地將兩個數字相加

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // 你會立即得到結果 = 3
非同步函數進行兩數相加
// 遠程將兩個數字相加

// 透過調用API獲取結果
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// 你得到的結果是 = "undefined"

如果你使用普通函數進行數字相加,你會立即得到結果。然而,當你發出遠程調用以獲取結果時,你需要等待,無法立即得到結果。

您無法確定是否會得到結果,因為伺服器可能會宕機、回應緩慢等。您不希望整個流程在等待結果時被阻塞。

調用API、下載文件和讀取文件是您將執行的一些常見的異步操作。

您不需要為異步呼叫使用Promise。在Promise之前,我們使用回調函數。回調函數是您在獲得返回結果時調用的函數。讓我們修改前面的例子以接受一個回調函數。

// 遠程添加兩個數字
// 通過調用API獲取結果

function addAsync (num1, num2, callback) {
    // 使用著名的jQuery getJSON回調API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // 回調函數
    const result = success; // 您在此處得到結果 = 3
});

後續的異步操作

我們希望將數字加三次,而不是一次加一個。在普通函數中,我們會這樣做:-

// 正常地添加兩個數字

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // 您立即得到resultA = 3
resultB = add(resultA, 3); // 您立即得到resultB = 6
resultC = add(resultB, 4); // 您立即得到resultC = 10

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

這是使用回調函數的外觀:

// 遠程添加兩個數字
// 通過調用API獲取結果

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // 使用著名的 jQuery getJSON 回調 API
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // 回調 1
    resultA = success; // 這裡你得到 result = 3

    addAsync(resultA, 3, success => {
        // 回調 2
        resultB = success; // 這裡你得到 result = 6

        addAsync(resultB, 4, success => {
            // 回調 3
            resultC = success; // 這裡你得到 result = 10

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

示例:https://jsbin.com/barimo/edit?html,js,console

這種語法由於深度嵌套的回調函數而較不友好。

避免深度嵌套的回調函數

Promise 可以幫助你避免深度嵌套的回調函數。讓我們看看同一示例的 Promise 版本:

// 使用可觀察對象遠程相加兩個數字

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // 使用 ES6 的 fetch API,它返回一個 Promise
	// .json() 是什麼?https://developer.mozilla.org/en-US/docs/Web/API/Body/json
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json()); 
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

使用 Promise,我們通過 .then 展平回調。在某種程度上,它看起來更乾淨,因為沒有回調嵌套。使用 ES7 的 async 語法,你可以進一步增強這個示例。

可觀察對象

在你決定使用 Promise 之前,有一種新興的技術可以幫助你處理異步數據,稱為 可觀察對象

讓我們看看用可觀察對象編寫的相同示例。在這個示例中,我們將使用 RxJS 來處理可觀察對象。

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // 使用 ES6 的 fetch API,它返回一個 Promise
    const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

可觀察對象可以做更多有趣的事情。例如,只需一行代碼就可以使用 delay 函數延遲 3 秒,或者重試,以便你可以重試某個調用一定的次數。

...

addAsync(1,2)
  .delay(3000) // 延遲 3 秒
  .do(x => resultA = x)
  ...

您可以閱讀我的一篇關於RxJs的文章這裡

結論

熟悉回調和承諾是非常重要的。理解它們並加以應用。暫且不必擔心可觀察對象。根據具體情況,這三者都可能成為您開發中的要素。

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises