cherryvenus

给初学者的 JavaScript Promise 教程 | Scotch

cherryvenus · 2016-12-15翻译 · 788阅读 原文链接

Code

Demo

Javascript Promises 不难。然而,许多人刚开始接触这个概念的时候,觉得有些难以理解。因此,我写下了我是如何理解 Promise 的,用一个通俗易懂的方法。

理解 Promise

Promise 简介:

"假设你是一个宝宝. 你的妈妈承诺(Promise)你,下个礼拜她会给你一台新手机。"

不知道,下个礼拜你是否会拿到手机。你的妈妈可以真的给你买一个全新的手机,或者放你鸽子,也有可能如果她不开心:(了就扣下了手机。

这个就是承诺(Promise)。一个 Promise 有3个状态。他们分别是:

  1. Promise 是待定的(pending): 你不知道你下个礼拜能不能拿到手机。
  2. Promise 是已解决的(resolved):你的妈妈真的给你买了一个全新的手机。
  3. Promise 是被拒绝的(rejected): 因为你妈妈不开心所以不给你手机了。

创建一个 Promise

让我们将这个转化为 JavaScript。

/* ES5 */
var isMomHappy = false;
// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // 完成了
        } else {
            var reason = new Error('妈妈不开心');
            reject(reason); // reject
        }

    }
);

代码本身颇具表现力。

  1. 我们用一个布尔值 isMomHappy,来定义妈妈是否开心。

  2. 我们有一个命名为willIGetNewPhone的 Promise 。这个 Promise 可以是 已完成的(resolved) (如果妈妈给你买了一个新手机)或者 被拒绝的(rejected) (妈妈不开心,她没有给你买一个)。

  3. 这里有一个标准的语法来新建一个 Promise,参考MDN 文档,一个 promise 语法看上去像这样。

// promise 语法看上去像这样
new Promise(/* executor*/ function (resolve, reject) { ... } );
  1. 你需要记住的是,当结果是正确的,在你的 promise 中调用 resolve(正确的值)。在我们的例子中,如果妈妈很开心,我们就会拿到手机。因此,我们调用 resolve 函数和 phone 这个变量。如果我们的妈妈不开心,我们会调用 reject 函数和一个理由(reason)reject(reason)

使用 Promise

现在,我们有一个 Promise。来看看怎么使用它:

/* ES5 */
...
// 调用我们的 Promise
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // 太好啦, 你获得了一个新手机
            console.log(fulfilled);
         // output: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // 好不幸,你妈妈没买手机
            console.log(error.message);
         // output: '妈妈不开心'
        });
};
askMom();
  1. 我们有一个名为 askMom 的函数。在这个函数中,我们会使用 Promise willIGetNewPhone

  2. 一旦 Promise 被解决(resolved)或者被拒(rejected),我门希望采取些措施。我们用 .then.catch 来实现。

  3. 在我们的例子中,.then 之中有个 function(fulfilled) { ... }fulfilled 是什么?fulfilled 就是是你传入 Promise 的 resolve(your_success_value).因此,在我们例子中就是 phone

  4. 我们在 .catch 中有 function(error){ ... }error 是什么?正如你猜测的,error 正是你传入 Promise 中的 reject(your_fail_value) 。因此,在我们的例子中就是 reason

让我们看看例子运行之后的结果吧!

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

例子

串联 Promise

Promiss 是可串联的。

也就是说,你,宝宝,承诺(Promise)你的小伙伴,当你妈妈给你买了手机,你就会给他们看新手机。

这就是另一个 Promise 啦。我们来写一个!

// 简略
...
// 第二个 promise
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'hey 伙计,我有个新 ' +
                phone.color + ' ' + phone.brand + '手机';
            resolve(message);
        }
    );
};

说明:

  • 在这个例子中,你可能意识到我们没有调用 reject。因为这个是可选的参数。

  • 我们可以简化这个样例就像用 Promise.resolve 代替。

// 简略
...

// 第二个 promise
var showOff = function (phone) {
    var message = 'hey 伙计,我有个新 ' +
                phone.color + ' ' + phone.brand + ' 手机';
    return Promise.resolve(message);
};

让我们串联 Promise。你,宝宝只能在willIGetNewPhone Promise 实现之后,才能开始 showOff Promise。

...
// 调用 Promise
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // 在这里串联
    .then(function (fulfilled) {
            console.log(fulfilled);
         // output: 'Hey 伙计, 我有一个新的黑色三星手机。'
        })
        .catch(function (error) {
            // 好不幸,你妈妈没买手机
            console.log(error.message);
         // output: '妈妈不开心'
        });
};

串联 Promise 简单吧!

Promises 是异步的

Promise 是异步的。让我们在调用 Promise 之前和之后各打印一个信息。

// 调用我们的Ppromise
var askMom = function () {
    console.log('询问妈妈之前'); // 运行之前打印
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('询问妈妈之后'); // 运行之后打印
}

预计的输出序列是怎么样的?也许你预计是这样的

1\. 询问妈妈之前
2\. Hey 伙计, 我有一个新的黑色三星手机。
3\. 询问妈妈之后

然而, 真实的输出顺序是这样的:

1\. 询问妈妈之前
2\. 询问妈妈之后
3\. Hey 朋友, 我有一个新的黑色三星手机。

输出

为什么? 因为生命 (或者 JavaScript) 不等人。

宝宝在玩的时候等待着妈妈的承诺(promise) (新手机).不是吗? 这个我们称之为 异步(asynchronous), 代码不会因为阻塞或等待结果而不运行. 任何想等待 Promise 之后再运行的, 你需要把他们放入 .then.

ES5, ES6/2015, ES7/Next 中的 promise

ES5 - 大多数浏览器

demo代码在 ES5 的环境(所有主流浏览器+NodeJs)中是可以运行,如果你包含了Bluebird Promise 库。这是因为 ES5 不支持直接调用 Promise 。另一个有名的 Promise 库是 Kris Kowal 的Q

ES6 / ES2015 - 现代浏览器, NodeJs v6

demo代码可以直接调用,因为ES6支持本地 Promise。外加,和 ES6 函数 fat arrow =>,以及 constlet 搭配使用,我们可以进一步简化代码`。

这里是 ES6 代码的例子

/* ES6 */
const isMomHappy = true;

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

    }
);

const showOff = function (phone) {
    const message = 'Hey 伙计, 我有个一个新' +
                phone.color + ' ' + phone.brand + '手机';
    return Promise.resolve(message);
};

// 调用我们的promise
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // fat arrow
        .catch(error => console.log(error.message)); // fat arrow
};

askMom();

注意,所有的var都用 const代替。所有的 function(resolve, reject) 都简化为(resolve, reject) =>。 这些改变有许多好处。阅读更多:

ES7 - 异步等待让语法看上去更整洁

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('妈妈不开心');
            reject(reason);
        }

    }
);

// 2nd promise
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey 伙计, 我有一个新' +
                phone.color + ' ' + phone.brand + '手机';

            resolve(message);
        }
    );
};

// 调用 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 askMom();
})();
  1. 每当你需要在函数中返回一个 Promise 的时候,你要在函数之前添加 async。 E.g. async function showOff(phone)

  2. 当你需要调用一个 promise,你需要在此之前添加 await。 E.g. let phone = await willIGetNewPhone; and let message = await showOff(phone);.

  3. 使用 try { ... } catch(error) { ... } 来捕捉 Promise 错误,被拒绝的 promise

为什么用 Promise 以及何时用他们?

为什么你需要 promise ?在 promise 之前我们是如何的?在回答这些问题之前,让我们回到基本原理。

普通函数 vs 异步函数

让我们看看这两个例子,他们都执行两个数字相加,一个用普通函数相加,一个用远程方法相加。

普通函数相加两个数字

// 正常相加数字

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

const result = add(1, 2); // you get result = 3 immediately
异步函数相加两个数字
// 远程相加数字

// 调用api获得结果
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// you get result  = "undefined"

如果你用普通函数相加两数字,你会马上获得结果。然而如果你发出一个远程调用来获得结果,那么你就需要等待,你不能马上得到结果。

或者这样说,你不知道会不会得到结果,因为服务器可能会性能下降,响应慢等等。你不希望因为等待着结果,让整个进程都被堵住。

调用API,下载文件,读取文件一些平时你会执行的异步操作。

Pomise 出现之前的世界: 回调(Callback)

我们一定要用 Prmoise 来做异步回调吗?不是的。优先于 promise ,我们用回调(callback)。回调(callback)仅仅是个你调用的函数,当你获得返回结果的时候。让我们修改之前的例子来获得一个回调。

// 远程相加两数字
// 调用API获得结果

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

addAsync(1, 2, success => {
    // callback
    const result = success; // 这里你得到 result = 3
});

这个语法看上去OK,为什么我们之后需要用 Promise ?

如果你想做一系列的异步操作怎么办??

比如说,不同于一次仅仅相加两个数字,我们希望加3次。用普通的函数,我们这么做:

// 普通相加两个数字

let resultA, resultB, resultC;

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

resultA = add(1, 2); // 你马上获得 resultA = 3
resultB = add(resultA, 3); // 你马上获得 esultB = 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) {
    // use the famous jQuery getJSON callback API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // you get result = 3 here

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // you get result = 6 here

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // you get result = 10 here

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

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

这个语法很不友好。更贴切地说,这个看上去像金字塔,人们经常称呼为 "回调地狱",因为一个回调嵌在另一个回调之中。想象你有10个回调,你的代码嵌套了10次!

逃离回调地狱

Promise 来拯救你了。让我们看一下相同例子的 Promise 的版本。

// 使用observables,远程相加连个数字

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // 使用 ES6 抓取API, 这个返回一个 promise
    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)
    });

Demo: https://jsbin.com/qafane/edit?js,console

和 promise 我们用.then让回调在统一层次上。在某种程度上,这个看上去更加干净了,因为没有回调嵌套。当然,用 ES7 async 语法,我们可以加强这个例子,但是我把它留给你。:)

新鲜出炉的属性: 监控属性(observables)

在你搞定 Promise 之前,有样叫做Observables的东西出现了,这让你处理异步数据更加简单。

Observables 是懒惰的事件流,这个你可以提交0个或者更多的事件,并且可能不结束。

Promises 和 observables 之间几点关键的差异:

  • Observables 是可以取消的
  • Observables 是懒惰的

不要怕,让我们看看用 Observables 写相同demo。在这个例子中,我为 Observables 使用 RxJS

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

function addAsync(num1, num2) {
    // 用 ES6 抓取 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)
  });

Demo: https://jsbin.com/dosaviwalu/edit?js,console

注意:

  • Observable.fromPromise 将一个 promise 转化为 Observable 流,
  • .do.flatMap 是 observables 提供的操作符中的两个。
  • 流是懒惰的。当我们 .subscribe 它的时候,addAsync 才会运行。

Observables 可以简单地处理恶心的东西。比如,delay 添加 3 seconds 函数,只有一行代码或者充实,因此你可以重试一个调用一定的次数。

...

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

好啦,让我们在未来的文章再讨论 observable 吧!

总结

你有没有熟悉了 callback 和 Promise 呢? 理解他们使用他们。不要担心 Observable, 也就这样。三个关键因素对你的开发来说,视情况而定。

这里是所有的 妈妈承诺买手机 demo的代码

就这样!希望这篇文章能够铲除你征服 JavaScript Promise 路途上的荆棘、。快乐码农~

相关文章