了解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。 这三个因素都可以根据情况影响您的发展。