三种方式理解Promise

Promise 的出现为一直以来饱受“回调地狱”噩梦困扰的异步编程带来了希望,但一开始接触这个概念,理解起来难免有些困难。本文讲述了三种帮助理解 Promise 的方式。

引子

下面是一个调用基于 Promise 的函数 asyncFunc() 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('DONE'), 100);
});
}
asyncFunc().then(x => console.log(`Result: ${x}`));
console.log('>_<');
// Output:
// >_<
// Result: DONE

所以呢,什么是 Promise?

  • 概念上,调用 asyncFunc() 是一个阻塞式函数调用
  • Promise 既是一个值的容器也是一个事件触发器

概念上:调用基于 Promise 的函数是阻塞的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function asyncFunc() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('DONE'), 100);
});
}
async function main() {
const x = await asyncFunc(); // (A)
console.log(`Result: ${x}`); // (B)
// Same as:
// asyncFunc().then(x => console.log(`Result: ${x}`));
}
main();

main() 是一个异步函数。其函数体很好地从概念上表明发生了什么——也是我们通常如何看待异步运算:

  • 行(A):等待,直到 asyncFunc() 执行完毕
  • 行(B):打印 x 的结果值到控制台日志

在 ES6 和 Generator 之前,你不能暂停并重新恢复代码执行。这就是为什么,对于 Promise,要将恢复代码执行后所发生的一切都置于回调函数中。调用回调与恢复代码是相同的。

Promise 是异步传递的值的容器

如果一个函数返回一个 Promise,那么这个 Promise 就像一个空的容器,函数一旦运算完毕,最后就会向其中填充它的结果。你可以通过数组模拟此过程的简单版本:

1
2
3
4
5
6
7
8
9
10
11
12
function asyncFunc() {
const blank = [];
setTimeout(() => blank.push('DONE'), 100);
return blank;
}
const blank = asyncFunc();
// 等待,直到值被填充
setTimeout(() => {
const x = blank[0]; // (A)
console.log(`Result: ${x}`);
}, 200);

使用 Promise,你不必通过 [0](像在行(A)中那样)访问最终值,你可以使用方法 then() 和回调函数。

Promise 是一个事件触发器

另一种看待 Promise 的方式是将其作为一个触发事件的对象。

1
2
3
4
5
6
7
8
9
10
11
function asyncFunc() {
const eventEmitter = { success: [] };
setTimeout(() => { // (A)
for (const handler of eventEmitter.success) {
handler('DONE');
}
}, 100);
return eventEmitter;
}
asyncFunc().success.push(x => console.log(`Result: ${x}`)); // (B)

注册事件监听器(行(B))可以在调用 asyncFunc() 之后完成,因为在这段代码完成后,交给 setTimeout()(行(A))的回调是异步执行的。

典型的事件触发器专门用于传递多个事件,从你注册时开始。

相比之下,Promise 专门提供一个确定值,并提供内置保护以防止注册太晚:Promise 的结果被缓存并传递到在 Promise 状态确定后注册的事件侦听器。

参考