理解Promise

cover_pic

Promise是ES6(又称ECMA2015)一个新引入的全局对象,用于一个异步操作的最终完成(或失败)及其结果值的表示,它使得异步代码的编写更为简单。

本文为译文,原文出处: https://scotch.io/tutorials/javascript-promises-for-dummies,作者通过本文生动的介绍了Promise是什么、如何使用以及为什么使用它来编写异步代码。

简介

“假设你是一个学生,你的妈妈承诺下周给你买一部手机。”
直到下周来之前,你都不知道是否会得到手机。但结果会是其中之一:

  • 你的妈妈真的给你买了
  • 由于其他原因而没有买成

这就是Promise(承诺),它是一个状态机,具有3个状态,分别是:

  • pending: 等待中,直到下周来之前,你不知道是否会得到新手机。
  • resolved: 符合期望,你的妈妈真的买了手机给你。
  • rejected: 期望落空,由于其他原因没有买成。

创建

让我们用JavaScript来陈述上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* ES5 */
var isMomHappy = false;
// Promise
var willIGetNewPhone = new Promise(
function (resolve, reject) {
if (isMomHappy) {
var phone = {
brand: '三星',
color: '黑色'
};
resolve(phone); // 期望达成
} else {
var reason = new Error('由于其他原因,不买手机了!');
reject(reason); // 期望落空
}
}
);
  1. isMomHappy:Boolean,不买手机的理由可以有很多,这得看妈妈的心情。
  2. willIGetNewPhone:Promise对象,它可以是resolve (期望达成) 或者reject (期望落空)。
  3. 这里使用标准语法来定义Promise对象,具体参考MDN,大概像这样:

    1
    2
    // promise syntax look like this
    new Promise(/* executor*/ function (resolve, reject) { ... } );
  4. 需要记住的是,当期望达成时,调用resolve(successValue),当期望落空时,调用reject(failValue)。在这个例子中,假设妈妈给我们买了一部新手机,因此调用了resolve(phone);否则我们调用reject(reason)。

使用

现在,我们已经有了一个Promise,接下来看看如何使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* ES5 */
...
// 调用Promise
var askMom = function () {
willIGetNewPhone
.then(function (fulfilled) {
// 😊期望达成,获取新手机
console.log(fulfilled);
// output: { brand: '三星', color: '黑色' }
})
.catch(function (error) {
// 😔期望落空,没有买成
console.log(error.message);
// output: '妈妈心情不好,不给买了'
});
};
askMom();
  1. 我们调用askMom方法来使用之前定义的Promise(willIGetNewPhone)
  2. 一旦Promise做出响应,我们将用.then或者.catch来进行相应的处理。
  3. .then里包含了一个function(fulfilled){ ... },那么fulfilled的值是什么,根据之前Promise的定义,我们得知这个值应该是phone,包含新手机的信息。
  4. .catch里也包含了一个function(error){ ... },根据定义,error的值应该是reason,是拒绝买手机的理由。

你可以在线运行这个Demo

链式调用

Promise可以被链式调用。
接下来你有了另外的想法,如果拿到新手机,就和小伙伴们炫耀一把,这时你可以创建另外一个Promise,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
// 第二个 promise
var showOff = function (phone) {
return new Promise(
function (resolve, reject) {
var message = '快看, 我有一个新的' +
phone.color + ' ' + phone.brand + '手机';
resolve(message);
}
);
};

在上面的Promise中,我们没有使用reject,因为它是可选的。

另外,我们还可以对它进行简化:

1
2
3
4
5
6
7
8
// 简化代码,使用Promise.resolve
var showOff = function (phone) {
var message = '快看, 我有一个新的' +
phone.color + ' ' + phone.brand + '手机';
return Promise.resolve(message);
};

接下来让我们链式地调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
// 调用Promise
var askMom = function () {
willIGetNewPhone
.then(showOff) // 把它链在这里
.then(function (fulfilled) {
// 😊期望达成,获取新手机
console.log(fulfilled);
// output: '快看, 我有一个新的黑色三星手机'
})
.catch(function (error) {
// 😔期望落空,没有买成
console.log(error.message);
// output: '妈妈心情不好,不给买了'
});
};

这里需要注意:只有在willIGetNewPhone达成,才会启动showOff。就像你没有拿到手机,也不会去跟小伙伴炫耀了,对吧?

异步执行

Promise是异步执行的,我们添加一些日志来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 调用我们的Promise
var askMom = function () {
console.log('Promise开始'); // log before
willIGetNewPhone
.then(showOff)
.then(function (fulfilled) {
console.log(fulfilled);
})
.catch(function (error) {
console.log(error.message);
});
console.log('Promise结束'); // log after
}

这里会输出是什么?更合理的应该是:

1
2
3
1. Promise开始
2. 快看, 我有一个新的黑色三星手机
3. Promise结束

实际输出的结果为:

1
2
3
1. Promise开始
2. Promise结束
3. 快看, 我有一个新的黑色三星手机

这个结果证实了Promise内部是异步执行的。好比在等待新手机的到来之前,并不会停止你现在的生活一样。同样,在Promise等待时,代码并不会阻塞,而是继续往下执行,如果需要在等待结果后处理的,请在.then中去完成。

(ES5/ES6/ES7)

ES5
因为ES5还不支持原生Promise,你需要引入 Bluebird 或者 Q 来兼容,它们在大多数ES5环境(浏览器和Node)都可以正常工作。

ES6/ES2015-主流浏览器、NodeJSv6
ES6可以支持原生Promise,无需引入任何其他第三方库。此外,你还可以使用新特性=>constlet来进一步简化代码:

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
28
29
30
31
32
33
34
35
/* ES6 */
const isMomHappy = true;
// Promise
const willIGetNewPhone = new Promise(
(resolve, reject) => { // 箭头函数
if (isMomHappy) {
const phone = {
brand: '三星',
color: '黑色'
};
resolve(phone);
} else {
const reason = new Error('妈妈心情不好,不给买了');
reject(reason);
}
}
);
const showOff = function (phone) {
const message = '快看, 我有一个新的' +
phone.color + ' ' + phone.brand + '手机';
return Promise.resolve(message);
};
// 调用Promise
const askMom = function () {
willIGetNewPhone
.then(showOff)
.then(fulfilled => console.log(fulfilled)) // 箭头函数
.catch(error => console.log(error.message)); // 箭头函数
};
askMom();

注意,所有var被换成了const,所有的function(resolve, reject)替换成了(resolve, reject) =>,这么做是有好处的。

ES7-async/await语法更简洁
ES7引入了asyncawait语法,看起来很像同步代码且符合逻辑,这比.then.catch的语法更加清晰和容易理解。

使用ES7语法重写上面的例子:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* ES7 */
const isMomHappy = true;
// 第一个 Promise
const willIGetNewPhone = new Promise(
(resolve, reject) => {
if (isMomHappy) {
const phone = {
brand: '三星',
color: '黑色'
};
resolve(phone);
} else {
const reason = new Error('妈妈心情不好,不给买了');
reject(reason);
}
}
);
// 第二个 promise
async function showOff(phone) {
return new Promise(
(resolve, reject) => {
var message = '快看, 我有一个新的' +
phone.color + ' ' + phone.brand + '手机';
resolve(message);
}
);
};
// 调用Promise
async function askMom() {
try {
console.log('Promise开始');
let phone = await willIGetNewPhone;
let message = await showOff(phone);
console.log(message);
console.log('Promise结束');
}
catch (error) {
console.log(error.message);
}
}
(async () => {
await askMom();
})();
  1. 每当需要返回一个Promise,在函数前加一个async,像async function askMom()
  2. 每当需要调用一个Promise,在前面加一个await,像let phone = await willGetNewPhonelet message = await showOff(phone)
  3. 使用try { ... } catch(error) { ... }将会捕获到Promise中的errorreject

解决哪些问题

为什么我们需要Promise?在回答这个问题之前,让我们回到原来写异步的方式,通过简单例子来说明:
实现两个函数,功能都是完成两个数值的相加,区别是:一个同步,一个异步:

两数相加的普通函数:

1
2
3
4
5
6
function add (num1, num2) {
return num1 + num2;
}
const result = add(1, 2); // you get result = 3 immediately

两数相加的异步函数,通过请求:

1
2
3
4
// 通过远程调用获取结果
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// result = "undefined"

上面例子中,通过普通函数的形式,我们会立马得到result。但是在异步中,需要等待结果的返回,所以无法立刻获得result,也就是说,在处理耗时操作时,像调用API、下载/读取文件一些常见的操作时,我们都需要等待,但又不想等待的过程中主进程被阻塞,因为还有其他的事情要做。在Promise出现以前,我们的解决方法是使用Callback,让我们修改上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过调用API获取结果
function addAsync (num1, num2, callback) {
// 通过jQuery的getJSON来调用API
return $.getJSON('http://www.example.com', {
num1: num1,
num2: num2
}, callback);
}
addAsync(1, 2, success => {
// callback
const result = success; // 3
});

它能正常工作,既然如此,还需要Promise吗?

接下来我们尝试对相加后的结果进行后续处理,假设我们对结果进行3次的累加操作:

同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 两数相加的普通函数
let resultA, resultB, resultC;
function add (num1, num2) {
return num1 + num2;
}
resultA = add(1, 2); // 3
resultB = add(resultA, 3); // 6
resultC = add(resultB, 4); // 10
console.log('total:', resultC); //total: 10
console.log(resultA, resultB, resultC); // 3, 6, 10

Demo: https://jsbin.com/barimo/edit?html,js,console
异步:

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
28
29
30
// 通过远程调用获取结果
let resultA, resultB, resultC;
function addAsync (num1, num2, callback) {
// 通过jQuery的getJSON来调用API
return $.getJSON('http://www.example.com', {
num1: num1,
num2: num2
}, callback);
}
addAsync(1, 2, success => {
// callback 1
resultA = success; // 3
addAsync(resultA, 3, success => {
// callback 2
resultB = success; // 6
addAsync(resultB, 4, success => {
// callback 3
resultC = success; // 10
console.log('total:', resultC);
console.log(resultA, resultB, resultC);
});
});
});

Demo: https://jsbin.com/qafane/edit?js,console
这种由 callback 嵌套另外一个 callback 组成的代码,第一眼看上去有些像金字塔,但人们通常称之为”回调地狱”,它的可读性非常差。你可以想象一下累加10次的代码长什么样。

逃离回调地狱:

让我们用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
27
28
let resultA, resultB, resultC;
function addAsync(num1, num2) {
// 使用ES6的fetch,将返回一个Promise
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)
});

Demo: https://jsbin.com/qafane/edit?js,console

通过Promise,我们将callback进行扁平化处理,不用再写类似回调地狱的代码了,还有更好的做法是使用ES7的async/await,但这留给你自己去实现。

总结:

当采用Promises后,程序变得非常清楚易读。
在实际项目中,经常需要编写大量的异步代码,然而在异步的处理(异步流)中却很容易丢失控制权,Promise可以很好解决这个问题。
觉得Promise不能够很好地处理复杂工作流?还可以试试RxJs
关于Promise的实现,请参考https://www.promisejs.org/implementing/。