JS Promise:保证厉害

天气渐渐的冷了,来点儿高能的干货,Javascript Promise…

Promise是JS开发圈子里的热门话题,通往高手的路上是一定要学会的东西。但是JS Promise还是有些难度的,需要比较多的理解和练习,才能最终掌握。

这篇文章会帮助你(当然也是帮助我自己)更好的理解Promise,促使你更多的使用。下面会解释Promise是个什么东西?它能解决什么问题,价值在哪里?还有如何使用?每一步都会用一个实际的代码段来解释,做到有码有真相。

What is a JavaScript promise?

其实说到底,Promise是一个构造函数,会生成并返回一个对象,这个对象代表一个值,但这个值在创建Promise的时候并不确定,需要根据异步操作的结果来决定。从本质上说,可以按照下面这样来解释:

promise.then(function(value) {
// Do something with the 'value'
});

Promise的逻辑更加清晰,而且相比之前的用来处理异步的回调函数,还有一些明显的好处,所以在JS的圈子里越来越流行,先是很多主流的JS框架开发了各自的Promise系统,之后原生JS也推出了自己的Promise方法,于是皆大欢喜了。

在这篇文章中,我们会纵览关于Promise的基本术语,然后通过实例来介绍背后的概念。

Get ready了吗?我们会掷很多骰子。。。。

What properties does a  promise have?

一个properties会有三个状态:pending, fulfilled, rejected。当被刚刚创建的时候,处于pending的状态。根据异步处理的结果成功与否,状态会有两种fulfilled或者rejected。这个状态转变的过程可以称为:resolution of the promise。

在原生JS里,创建一个promise对象的方法,和创建其他对象没有什么不同,就是通过new Promise() 构造函数就可以了,这个构造函数会接收一个函数作为参数,而这个函数又有两个参数:resolve,reject,而在Promise的定义里,这两个参数本身,也是函数。参数名不是关键的,关键的是对这两个函数的处理。一般来说,如果异步处理一切正常,通过调用执行resolve(),promise会fulfill。如果异步处理不成功,那末就调用执行reject(),这样就将promise的状态设置为reject。如下:

var promise = new Promise(function(resolve, reject) {
asynchronous code goes here
if(everything is ok) {
resolve(responds);
} else {
reject(responds);
}
});

所以在处理异步操作的时候,我们可以把异步操作的工作包裹在promise构造函数中,并返回这个对象。根据异步操作的结果,进行相应的逻辑判断,然后调用resolve或者reject,这里并不需要对这两个函数写具体的内部逻辑,具体的实现放在后面的then函数中进行。关键是把需要的数据当作参数付给这两个函数。

到目前为止,就算是构建好了一个Promise,那如何使用它呢?

需要用Promise对象的then()方法,这个方法也接受两个函数作为参数,分别对应resolve和reject,而函数接收的参数跟Promise构造函数中传给resolve和reject相同。根据Promise的状态是fulfilled或者rejected,来调用相应的函数。开始掷骰子。。。

例子1
function dieToss(){
return Math.floor(Math.random()*6) + 1
}
console.log('1');
var promise = new Promise(function(resolve, reject){
var n;
setTimeout(function(n){
n = dieToss();
if (n === 6){
resolve(n);
} else {
reject(n);
}
console.log('2');
},1000);
console.log('3');
});
promise.then(function(toss){
console.log('Yay, threw a ' + toss + '.');
}, function(toss){
console.log('Oh,noes, threw a ' + toss + '.');
});
console.log('4');

上面这段代码的打印结果可能是:
1
3
4
2
Oh, noes, threw a 4.

如果运气不错,还有可能是:
1
3
4
2
Yay, threw a 6.

上面这段代码说明了两件事情。
首先,then函数是最后一个执行的。另外,then里面的resolve函数只有在promise是fulfilled状态才会被执行。对于reject的也是一样的。

Chaining promises and  trickling down?

如果想要实现下面这段代码的promise chain的效果,就要求then也要返回一个promise,这样就可以串联多个then了,这样从宏观上看,就像用同步的方式处理连续的非同步事件,非常优雅。
signupPayingUser
.then(displayHoorayMessage)
.then(queueWelcomeEmail)
.then(queueHandwrittenPostcard)
.then(redirectToThankYouPage)

上面这个虚构的例子中,signupPayingUser首先返回一个promise,然后在这个链条上的每个函数都会被调用,而每个函数本身应该返回一个Promise,触发下一个then函数。

为了分析这个过程的细节,继续掷骰子。现在的情况是我们会连续掷骰子,最多三次,如果得到最大的6点就停止,代码如下:

function dieToss(){
return Math.floor(Math.random()*6) + 1
}

function tossASix(){
return new Promise(function(resolve,reject ){
var n = dieToss();
if(n==6) {
resolve(n);
} else {
reject(n);
}
});
}

function logAndTossAgain(toss) {
console.log("Tossed a" + toss + ", need to try again.");
return tossASix();
}

function logSuccess(toss) {
console.log("Yay, managed to toss a " + toss + ".");
}

function logFailure(toss) {
console.log("Tossed a " + toss + ". Too bad, couldn't roll a six");
}

tossASix()
.then(null, logAndTossAgain)   //Roll first time
.then(null, logAndTossAgain)   //Roll second time
.then(logSuccess, logFailure); //Roll third and last time

运行上面的代码,得到的结果有可能是:
Tossed a 2, need to try again.
Tossed a 1, need to try again.
Tossed a 4. Too bad, couldn't roll a six.

出现这样的结果还是比较好理解的,因为第一次得到的数字是2,所以tossASix返回的promise被reject,调用logAndTossAgain会打印当前结果然后再返回一个promise。按照这个逻辑在这个链条上传递下去。

这个例子还有一个值得注意的点。就是如果第一次就得到了6点,是如何停止继续掷骰子的呢?注意到在这个promise的链条上的resolve句柄,除了最后一个是logSuccess,其他都是null。这里面的机制是如果resolve句柄不是一个函数,那末当前promise状态就会传递下去直到到达一个函数句柄为止。如果把null都改成logSuccess,结果是什么样呢?回报错的,因为logSuccess并没有返回一个promise,所以下一个then的存在就没有根据了。

Handling error?

利用上面说的promise在链条上进行传递的机制,可以很优雅的处理error,比如下面这样:

signupPayingUser
.then(displayHoorayMessage)
.then(queueWelcomeEmail)
.then(queueHandwrittenPostcard)
.then(redirectToThankYouPage)
.then(null, displayAndSendErrorReport)

只有在链条的最后一个环节加上reject句柄,那末在链条的中间过程中,如果出现异常,会最终传给最后的那个displayAndSendErrorReport.

看一个实际的例子,把骰子继续到底:

var tossTable = {
1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six'
};

function toss(){
return new Promise(function(resolve,reject){
var n = Math.floor(Math.random() * 6) + 1;
resolve(n);
});
}

function logErrorMessage(error) {
console.log("Oops: " + error.message);
}

function logAndTossAgain(toss){
var tossWord = tossTable[toss];
console.log("Tossed a" + tossWord.toUppercase() + ".");
}

toss()
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(logAndTossAgain);

在这个例子里,每次掷骰子都会直接调用resolve(),相当于每个promise都会resolve,这里只是举个例子而已。
上面的代码中有一个bug,就是toUppercase,应该是toUpperCase, 但是因为没有error处理的部分,所以结果什么都木有打印。。。

修改一下上面的代码,把toUpperCase()写正确,然后在链条的最后加上error handling的部分,变成下面这样:
var tossTable = {
1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six'
};

function toss(){
return new Promise(function(resolve,reject){
var n = Math.floor(Math.random() * 6) + 1;
resolve(n);
});
}

function logErrorMessage(error) {
console.log("Oops: " + error.message);
}

function logAndTossAgain(toss){
var tossWord = tossTable[toss];
console.log("Tossed a" + tossWord.toUppercase() + ".");
}

function logErrorMessage(error) {
console.log("Oops: " + error.message);
}

toss()
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(null, logErrorMessage)

运行上面修改过的代码段,会得到类似的结果:

"Tossed a TWO."
"Oops: Cannot read property 'toUpperCase' of undefined"

第一个logAndTossAgain可以成功运行,但是它没有返回任何promise,所以下一个promise以undefined的值resolve了。接着第二个resolve句柄触发后,报出了这个错误。所以此处的经验是,如果要使用chain的方式,resolve和reject函数一定要返回一个新的promise。

Using promises to solve real problems?

当然在真实的应用中,promise会处理要远比掷骰子复杂的多的问题。比如把上面例子里的掷骰子过程换成API异步请求,那就会复杂很多了。promise用好了,写出的code会更加容易读,也容易debug,对于需要大量处理异步事件的前端开发来说,绝对有价值的。

原文链接:https://www.toptal.com/javascript/javascript-promises