了解JavaScript承诺
介绍
Javascript Promises 可能很难理解。 因此,我想写下我对 Promise 的理解方式。
理解 Promise
简短的承诺:
“想象一下,你是一个 孩子。 你妈妈 向你承诺 下周她会给你买一部 新手机 。”
你 不知道 直到下周你才能拿到那部手机。 你妈妈可以真的给你买一部全新的手机,或者她不会。
那是 承诺 。 一个承诺具有三种状态。 他们是:
- 待定:你不知道你是否会得到那部手机
- 实现:妈妈很高兴,她给你买了一部全新的手机
- 被拒绝:妈妈不开心,她不给你买手机
创建一个承诺
让我们将其转换为 JavaScript。
// ES5: Part 1 var isMomHappy = false; // Promise var willIGetNewPhone = new Promise( function (resolve, reject) { if (isMomHappy) { var phone = { brand: 'Samsung', color: 'black' }; resolve(phone); // fulfilled } else { var reason = new Error('mom is not happy'); reject(reason); // reject } } );
代码本身就很有表现力。
下面是 Promise 语法的正常外观:
// promise syntax look like this new Promise(function (resolve, reject) { ... } );
消费承诺
现在我们有了 Promise,让我们使用它:
// ES5: Part 2 var willIGetNewPhone = ... // continue from part 1 // call our promise var askMom = function () { willIGetNewPhone .then(function (fulfilled) { // yay, you got a new phone console.log(fulfilled); // output: { brand: 'Samsung', color: 'black' } }) .catch(function (error) { // oops, mom didn't buy it console.log(error.message); // output: 'mom is not happy' }); }; askMom();
让我们运行示例并查看结果!
演示:https://jsbin.com/nifocu/1/edit?js,console
链接承诺
Promise 是可链接的。
假设你,孩子,承诺你的朋友,当你妈妈给你买一部新手机时,你会给他们新手机。
那是另一个承诺。 让我们写吧!
// ES5 // 2nd promise 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); } ); };
注意:我们可以通过编写如下来缩短上面的代码:
// shorten it // 2nd promise var showOff = function (phone) { var message = 'Hey friend, I have a new ' + phone.color + ' ' + phone.brand + ' phone'; return Promise.resolve(message); };
让我们链接承诺。 你这个孩子,只能在 willIGetNewPhone
承诺之后开始 showOff
承诺。
// call our promise var askMom = function () { willIGetNewPhone .then(showOff) // chain it here .then(function (fulfilled) { console.log(fulfilled); // output: 'Hey friend, I have a new black Samsung phone.' }) .catch(function (error) { // oops, mom don't buy it console.log(error.message); // output: 'mom is not happy' }); };
这就是你可以链接承诺的方式。
Promise 是异步的
Promise 是异步的。 让我们在调用 Promise 之前和之后记录一条消息。
// call our promise var askMom = function () { console.log('before asking Mom'); // log before willIGetNewPhone .then(showOff) .then(function (fulfilled) { console.log(fulfilled); }) .catch(function (error) { console.log(error.message); }); console.log('after asking mom'); // log after }
预期输出的顺序是什么? 你可能期望:
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.
在等待妈妈的承诺(新手机)时,您不会停止玩耍。 这就是我们所说的异步:代码将运行而不会阻塞或等待结果。 任何需要等待 Promise 进行的东西都放在 .then
中。
这是 ES5 中的完整示例:
// ES5: Full example var isMomHappy = true; // Promise var willIGetNewPhone = new Promise( function (resolve, reject) { if (isMomHappy) { var phone = { brand: 'Samsung', color: 'black' }; resolve(phone); // fulfilled } else { var reason = new Error('mom is not happy'); reject(reason); // reject } } ); // 2nd promise var showOff = function (phone) { var message = 'Hey friend, I have a new ' + phone.color + ' ' + phone.brand + ' phone'; return Promise.resolve(message); }; // call our promise var askMom = function () { willIGetNewPhone .then(showOff) // chain it here .then(function (fulfilled) { console.log(fulfilled); // output: 'Hey friend, I have a new black Samsung phone.' }) .catch(function (error) { // oops, mom don't buy it console.log(error.message); // output: 'mom is not happy' }); }; askMom();
ES5、ES6/2015、ES7/Next 中的 Promise
ES5 - 主流浏览器
如果您包含 Bluebird 承诺库,则演示代码可在 ES5 环境(所有主要浏览器 + NodeJs)中运行。 这是因为 ES5 不支持开箱即用的 Promise。 另一个著名的 promise 库是 Kris Kowal 的 Q。
ES6 / ES2015 - 现代浏览器,NodeJs v6
演示代码开箱即用,因为 ES6 原生支持 Promise。 另外,使用 ES6 函数,我们可以进一步简化代码,用箭头函数,使用 const
和 let
。
这是 ES6 代码中的完整示例:
//_ ES6: Full example_ 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); } } ); // 2nd promise const showOff = function (phone) { const message = 'Hey friend, I have a new ' + phone.color + ' ' + phone.brand + ' phone'; return Promise.resolve(message); }; // call our 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 引入了 async
和 await
语法。 它使异步语法更容易理解,没有 .then
和 .catch
。
用 ES7 语法重写我们的例子:
// ES7: Full example 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); } } ); // 2nd 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); } ); }; // call our promise in ES7 async await style 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 it here too (async () => { await askMom(); })();
承诺以及何时使用它们
为什么我们需要承诺? 世界在承诺面前是怎样的? 在回答这些问题之前,让我们回到基本面。
普通函数 VS 异步函数
我们来看看这两个例子。 这两个示例都执行两个数字的相加:一个使用普通函数相加,另一个远程相加。
两个数字相加的普通函数
// add two numbers normally function add (num1, num2) { return num1 + num2; } const result = add(1, 2); // you get result = 3 immediately
两个数字相加的异步函数
// add two numbers remotely // get the result by calling an API const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2'); // you get result = "undefined"
如果你用普通函数添加数字,你会立即得到结果。 但是,当你发出远程调用获取结果时,需要等待,并且不能立即得到结果。
您不知道是否会得到结果,因为服务器可能已关闭、响应缓慢等。 您不希望在等待结果时阻止整个过程。
调用 API、下载文件和读取文件是您将执行的一些常见异步操作。
您不需要对异步调用使用 Promise。 在 Promise 之前,我们使用回调。 回调是您在获得返回结果时调用的函数。 让我们修改前面的示例以接受回调。
// add two numbers remotely // get the result by calling an API 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 const result = success; // you get result = 3 here });
后续异步操作
我们不想一次添加一个数字,而是要添加三次。 在正常功能中,我们会这样做:-
// add two numbers normally let resultA, resultB, resultC; function add (num1, num2) { return num1 + num2; } resultA = add(1, 2); // you get resultA = 3 immediately resultB = add(resultA, 3); // you get resultB = 6 immediately resultC = add(resultB, 4); // you get resultC = 10 immediately console.log('total' + resultC); console.log(resultA, resultB, resultC);
这就是回调的样子:
// add two numbers remotely // get the result by calling an API let resultA, resultB, resultC; function addAsync (num1, num2, callback) { // use the famous jQuery getJSON callback API // https://api.jquery.com/jQuery.getJSON/ 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); }); }); });
演示:https://jsbin.com/barimo/edit?html,js,console
由于深度嵌套的回调,这种语法对用户不太友好。
避免深度嵌套的回调
Promise 可以帮助您避免深度嵌套的回调。 让我们看一下同一个例子的 Promise 版本:
// add two numbers remotely using observable let resultA, resultB, resultC; function addAsync(num1, num2) { // use ES6 fetch API, which return a promise // What is .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
语法,您可以进一步增强此示例。
可观察的
在你接受承诺之前,有一些东西可以帮助你处理异步数据,称为 Observables
。
让我们看一下使用 Observables 编写的同一个演示。 在这个例子中,我们将使用 RxJS 作为 observables。
let Observable = Rx.Observable; let resultA, resultB, resultC; function addAsync(num1, num2) { // use ES6 fetch API, which return a 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) });
Observables 可以做更多有趣的事情。 例如,delay
通过 3 seconds
添加功能只需一行代码或重试,以便您可以重试调用一定次数。
... addAsync(1,2) .delay(3000) // delay 3 seconds .do(x => resultA = x) ...
您可以在 此处 阅读我的一篇 RxJs 帖子。
结论
熟悉回调和承诺很重要。 了解它们并使用它们。 暂时不要担心 Observables。 这三个因素都可以根据情况影响您的发展。