理解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();
})();

何时使用Promise

为何我们需要Promise?在Promise出现之前,世界是怎样的?在回答这些问题之前,让我们回归基础。

普通函数与异步函数对比

来看这两个示例。两个示例都执行了两个数字的加法:一个使用普通函数进行加法,另一个则是远程加法。

普通函数加法示例

// 普通方式相加两个数字

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; // 此处得到结果 = 3

    addAsync(resultA, 3, success => {
        // 回调2
        resultB = success; // 此处得到结果 = 6

        addAsync(resultB, 4, success => {
            // 回调3
            resultC = success; // 此处得到结果 = 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
	// .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)
    });

通过承诺,我们用 .then 展平回调。从某种程度上看,这显得更整洁,因为没有回调嵌套。使用 ES7 的 async 语法,你可以进一步优化这个示例。

可观察对象

在你决定使用承诺之前,有一种新兴技术可以帮助你处理异步数据,称为 可观察对象

让我们看看用可观察对象编写的同一个演示。在这个例子中,我们将使用 RxJS 作为可观察对象的实现。

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

function addAsync(num1, num2) {
    // 使用返回承诺的 ES6 fetch API
    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的文章。

结论

熟悉回调函数和Promise至关重要。理解并运用它们。暂时不必担心Observable。根据具体情况,这三种技术都可以在你的开发中发挥作用。

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