深入Promise

Promise的重要性应该不用多说,大家用的频率也很高,因此有必要深入了解一下。

Promise应运而生

在实际的使用当中,有非常多的应用场景我们不能立即知道应该如何继续往下执行。最重要也是最主要的一个场景就是ajax请求。通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 简单的ajax原生实现
var url = 'http://someRequest';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);
}
}

在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发并且符合一定条件时,才能拿到我们想要的数据,之后我们才能开始处理数据。

这样做看上去并没有什么麻烦,但是如果这个时候,我们还需要做另外一个ajax请求,这个新的ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候我们就不得不如下这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var url = 'http://someRequest';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);

// 伪代码
var url2 = 'http://someRequest?id=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}

当出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,我们的代码就变成了一场灾难。这场灾难,往往也被称为回调地狱。当然,除了回调地狱之外,还有一个非常重要的需求:为了我们的代码更加具有可读性和可维护性,我们需要将数据请求与数据处理明确的区分开来。

因此我们需要一个叫做Promise的东西,来解决这个问题。

Promise基本概念

Promise对象有三种状态,他们分别是:

  • pending: 等待中,或者进行中,表示还没有得到结果
  • resolved(fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
  • rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行

这三种状态不受外界影响,而且状态只能从pending改变为resolved或者rejected,并且不可逆。在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。

1
2
3
4
new Promise(function(resolve, reject) {
if(true) { resolve() };
if(false) { reject() };
})

Promise对象中的then方法,可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法,第一个then方法完成以后,会将返回结果作为参数,传入第二个then方法。

1
2
3
4
5
6
7
8
9
10
promise('aaa')
.then(function() {
console.log('成功1');
//……
return promise('bbb');
})
.then(function() {
console.log('成功2');
//……
});

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

1
2
3
4
5
6
promise('aaa').then(function() {
// ...
}).catch(function(error) {
// 处理 promise 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

注意,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。因此一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。


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

1
2
3
4
5
6
7
8
9
10
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return promise(id);
});

Promise.all(promises).then(function (res) {
// ...
}).catch(function(reason){
// ...
});

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

对ajax进行封装

OK,了解了这些基础知识之后,我们再回过头,利用Promise的知识,对最开始的ajax的例子进行一个简单的封装。看看会是什么样子。

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
27
var url = 'http://someRequest';

// 封装一个get请求的方法
function getJSON(url) {
return new Promise(function(resolve, reject) {
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
if (XHR.readyState == 4) {
if (XHR.status == 200) {
try {
var response = JSON.parse(XHR.responseText);
resolve(response);
} catch (e) {
reject(e);
}
} else {
reject(new Error(XHR.statusText));
}
}
}
})
}

getJSON(url).then(resp => console.log(resp));

为了健壮性,处理了很多可能出现的异常,总之,就是正确的返回结果,就resolve一下,错误的返回结果,就reject一下。并且利用上面的参数传递的方式,将正确结果或者错误信息通过他们的参数传递出来。

实现一个简单的Promise

看到这里,我们已经对Promise有了大概的认识,接着我们可以试着自己实现一个简易版的Promise,加深对它的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Promise(fn) {
var value = null,
callbacks = []; //callbacks为数组,因为可能同时有很多个回调

this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};

function resolve(value) {
callbacks.forEach(function (callback) {
callback(value);
});
}

fn(resolve);
}

此时已经有基本的resolve和then了,再实现then的链式调用

1
2
3
4
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};

以上代码存在一个明显的问题:如果在then方法注册回调之前,resolve函数就执行了,那then的回调函数将不会执行,且Promise异步操作成功之后调用的then注册的回调也不会执行了。因此加入延时机制,还需要加入状态:

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
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];

this.then = function (onFulfilled) {
if (state === 'pending') {
callbacks.push(onFulfilled);
return this;
}
onFulfilled(value); //如果是resolve后调用then,那么立即执行回调函数
return this;
};

function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
callbacks.forEach(function (callback) {
callback(value);
});
}, 0);
}

fn(resolve);
}

我们暂时忽略了错误处理机制,为的使我们更关注于核心代码上面去

相关阅读:Promise源码