前端静径

promise学习笔记

前言

一直想写一篇关于promise的文来总结一下之前零零散散的promise知识点,趁着工作闲暇,来做个总结。PS:本文适合有一定JavaScript基础的童鞋阅读。

什么是promise

如果说到JavaScript的异步处理,我想大多数人都会想到利用回调函数:

1
2
3
4
5
6
7
//示例:使用了回调函数的异步处理
http.get('/v1/get', function(error, data) {
if (error) {
//错误时的处理
}
//成功时的处理
})

像上面这样基于回调函数的异步处理如果统一参数使用规则的话,写法也会很明了。但是,这也仅是编码规约而已,即使采用不同的写法也不会报错。

Promise则是把类似的异步处理对象和处理规则进行规范化,并按照采用统一的接口来编写,而采取规定方法之外的写法都会报错。下面看一个例子:

1
2
3
4
5
6
var promise = http.get('/v1/get');
promise.then(function(result) {
//成功时的处理
}).catch(function(error) {
//错误时的处理
})

可以看到,这里在使用promise进行一步处理的时候,我们必须按照接口规定的方法编写处理代码。也就是说,除promise对象规定的方法(这里的 thencatch)以外的方法都是不可以使用的, 而不会像回调函数方式那样可以自己自由的定义回调函数的参数,而必须严格遵守固定、统一的编程方式来编写代码。这样,基于Promise的统一接口的做法, 就可以形成基于接口的各种各样的异步处理模式。

但这并不是使用promise的足够理由,promise为异步操作提供了统一的接口,能让代码不至于陷入回调嵌套的死路中,它的强大之处在于它的链式调用(文章后面会有提及)。

基本用法

promise的语法:

1
2
3
4
new Promise(function(resolve, reject) {
//待处理的异步逻辑
//处理结束后,调用resolve或reject方法
})

新建一个promise很简单,只需要new一个promise对象即可。所以promise本质上就是一个函数,它接受一个函数作为参数,并且会返回promise对象,这就给链式调用提供了基础。

其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。而这些实例有以下三种状态:

  1. pending: 初始状态,位履行或拒绝
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

pending 状态的 Promise对象可能以 fulfilled状态返回了一个值,也可能被某种理由(异常信息)拒绝(reject)了。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers)就会被调用,then方法分别指定了resolve方法和reject方法的回调函数

一图胜千言:

alt promise图解

简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// 如果调用了resolve方法,执行此函数
}, function(value) {
// 如果调用了reject方法,执行此函数
});

上述代码很清晰的展示了promise对象运行的机制。下面再看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});

上面代码中,resolve方法和reject方法调用时,都带有参数。它们的参数会被传递给回调函数。reject方法的参数通常是Error对象的实例,而resolve方法的参数除了正常的值以外,还可能是另一个Promise实例,比如像下面这样。

1
2
3
4
5
6
7
8
var p1 = new Promise(function(resolve, reject){
// ... some code
});
var p2 = new Promise(function(resolve, reject){
// ... some code
resolve(p1);
})

上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,这时p1的状态就会传递给p2。如果调用的时候,p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是fulfilled或者rejected,那么p2的回调函数将会立刻执行。

promise的链式操作

正如前面提到的,Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。

1
2
3
4
5
getJSON("/visa.json").then(function(json) {
return json.name;
}).then(function(name) {
// proceed
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

1
2
3
4
5
getJSON("/visa/get.json").then(function(post) {
return getJSON(post.jobURL);
}).then(function(jobs) {
// 对jobs进行处理
});

这种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的“横向发展”改为“向下发展”。

promise捕获错误

Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。

1
2
3
4
5
6
getJSON("/visa.json").then(function(result) {
// some code
}).catch(function(error) {
// 处理前一个回调函数运行时发生的错误
console.log('出错啦!', error);
});

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

1
2
3
4
5
6
7
getJSON("/visa.json").then(function(json) {
return json.name;
}).then(function(name) {
// proceed
}).catch(function(error) {
//处理前面任一个then函数抛出的错误
});

其他常用的promise方法

Promise.all方法,Promise.race方法

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

1
var p = Promise.all([p1,p2,p3]);

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例。(Promise.all方法的参数不一定是数组,但是必须具有iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子。

1
2
3
4
5
6
7
8
9
10
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
return getJSON("/get/addr" + id + ".json");
});
Promise.all(promises).then(function(posts) {
// ...
}).catch(function(reason){
// ...
});

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。

1
var p = Promise.race([p1,p2,p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

Promise.resolve方法,Promise.reject方法

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

1
var jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代码将jQuery生成deferred对象,转为一个新的ES6的Promise对象。

如果Promise.resolve方法的参数,不是具有then方法的对象(又称thenable对象),则返回一个新的Promise对象,且它的状态为fulfilled。

1
2
3
4
5
6
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello

上面代码生成一个新的Promise对象的实例p,它的状态为fulfilled,所以回调函数会立即执行,Promise.resolve方法的参数就是回调函数的参数。

如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

1
2
3
4
5
6
var p = Promise.reject('出错啦');
p.then(null, function (error){
console.log(error)
});
// 出错了

上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。

题外话:async函数

async函数是es7提案出来的语法,并不属于es6,但是已经有一些平台和编辑器支持这种函数了,所以这里也做一下了解。

async函数是用来取代回调函数的另一种方法。

只要函数名之前加上async关键字,就表明该函数内部有异步操作。该异步操作应该返回一个Promise对象,前面用await关键字注明。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

1
2
3
4
async function getStockPrice(symbol, currency) {
let price = await getStockPrice(symbol);
return convert(price, currency);
}

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数将返回一个Promise对象。调用该函数时,当遇到await关键字,立即返回它后面的表达式(getStockPrice函数)产生的Promise对象,不再执行函数体内后面的语句。等到getStockPrice完成,再自动回到函数体内,执行剩下的语句。

下面是一个更一般性的例子。

1
2
3
4
5
6
7
8
9
10
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncValue(value) {
await timeout(50);
return value;
}

上面代码中,asyncValue函数前面有async关键字,表明函数体内有异步操作。执行的时候,遇到await语句就会先返回,等到timeout函数执行完毕,再返回value。

个人觉得async函数将异步发挥到了极致,代码看上去更加简洁,更加舒服了,而且其流程也很好理解。

总结

promise作为异步操作的规则,确实给开发带来了不少便利,至少不用像回调那样,出现函数里面套函数这种无限的嵌套的情况,promise让你的代码变得更加的优雅了。当然如果之后async函数变得更加普及,那么就更好了。

下面来看看下面这道题目,大家可以思考下结果是多少?

1
2
3
4
5
6
7
8
9
10
11
12
console.log(1);
new Promise(function (resolve, reject){
reject(true);
window.setTimeout(function (){
resolve(false);
}, 0);
}).then(function(){
console.log(2);
}, function(){
console.log(3);
});
console.log(4);

答案我就不贴出来了,给大家思考的空间,实在不知道结果的也可以直接复制这段代码到浏览器控制台执行下,也能很快的出结果。

展望

很好奇promise内部的实现机理,接下来我会深入研究下promise实现原理,有成果了给大家分享哦~