Promise 与 RxJS(async/await)

Promise与RxJS对比

相似处

他们都解决了原来代码的多层嵌套或者是不断的callback,使代码看起来更优雅也便于维护。

下面是两个示例:

Promise示例

1
2
3
4
5
6
let promise = new Promise(resolve => {
setTimeout(() => {
resolve('---promise timeout---');
}, 2000);
});
promise.then(value => console.log(value));

RxJS

1
2
3
4
5
6
7
8
9
10
let stream1$ = new Observable(observer => {
let timeout = setTimeout(() => {
observer.next('observable timeout');
}, 2000);

return () => {
clearTimeout(timeout);
}
});
let disposable = stream1$.subscribe(value => console.log(value));

区别

  • Promise顾名思义,提供的是一个允诺,这个允诺就是在调用then之后,它会在未来某个阶段把异步函数执行的结果传给then里的函数。
    Rx不是允诺,它本质上还是由订阅发布模式印出来的,核心思想就是数据响应式。
    (源头)数据产生者 => 一系列的变换/过滤/合并(操作) => 数据消费者使用,数据消费者何时使用,完全取决于数据流何时能流下来。

  • Promise用then链来处理数据,包括对数据进行过滤、合并、变换等操作,它没有真正意义上的数据消费者,then链的每一个都是数据消费者,所以它非常适合组装流程式,也就是说A完成之后做B,然后B完成后去完成C这种流程,这些流程可以无穷无尽,没有底的。
    Rx有数据产生的源头和严格意义的数据消费者,数据可以在中间的操作符里被处理,比如说做过滤,做合并,做节流,变换成新的数据源头等等,可以把它想象成一个完整的数据链,有头也有尾,到了最终消费者那边这个数据流就算到底。

  • Promise需要调用then或catch才能够执行,catch是then的另一种形式,调用then或者catch之后如果返回一个新的Promise,新的Promise又可以被调用,因此可以做成无限的then链。
    Rx数据是否流出不取决于subscribe,一个observable在未被订阅的时候也可以流出数据,在之后被订阅后先前流出的数据无法被消费者查知的,所以Rx引入了一个lazy模式,允许数据缓存着知道被订阅,但数据是否流出并不依赖subscribe。observable被订阅后并不是返回新的observable,而是返回一个subsciber,这样可以取消订阅,但是也导致了链式断裂,所以不能像Promise一样组成无线then链。

  • Promise数据是一次性流出的,因为Promise内部维持着状态,初始化的pending,转成resolved或者rejected之后,状态就不可逆转了。
    举例说promise().then(A).then(B).then(C).catch(D),数据是顺着链以此传播,但是只有一次,数据从A到B之后,A这个promise的状态发生了改变,从pedding转成了resolved,那么它就不可能再产生内容了,所以这个promise已经不是活动性的了。
    而Rx则不同,我们从Rx的接口就可以知道,它有onNext,onComplete和onError,onNext可以响应无数次,这也是符合我们对数据响应式的理解,数据在源头被隔三差五的发出,只要源头认为没有流尽(onComplete)或者出了问题(onError),那么数据就可以不断的流到响应者那边。

  • Promise的then链里面,每一行都是同样的角色,也就是Promise,所以它既可以是源头,也可以是数据处理者。
    Rx这边的observable还有一些变种,比如说常用的subject,它可以充当双面角色,可以订阅也可以发消息,这样的话我们还可以用它来做很多封装的工作。

Promise和Rx这两个模式的思想差别很清晰,一个是流程式,一个是数据响应式。
Promise可以用来贯串一连串单一的流程,而且这个流程是可以无限的,而Rx是用一个数据流来贯串所有操作符,它有一个真正意义上的数据消费者。

应用场景

我们在哪些场景下用Rx比较方便?

首先是需要源源不断的流出数据的场景,因为Promise是一次性的,不适合做这类工作。
比如说把事件/定时器抽象成Rx的Observable更合适,事件可以响应很多次,定时器也可以响应很多次,我们还可以利用Rx的debounce运算符来进行节流,在频繁触发事件的时候过滤那些重复的。

例如:验证码倒计时用Rx较合适,倒计时过程中要一直更新时间,结束后要重新改变按钮的文字及状态。

其次是可能需要重试的场景,由于Rx有retry或者repeat这种从源头开始的运算符,我们可以用它来执行比如“出错后重试三次”之类动作,而Promise就需要你递归处理了,破坏了then的链式。

例如:请求接口的重试或者是按钮点击多次只生效一次(避免按钮重复点击)

而Promise也有一些优于Rx的场景

例如:提交前要先上传图片或者是一个接口的参数取决于另两个接口的返回。

结论

这两种模式都有自己的想法,所以在使用Rx的时候,不要把它当成Promise来用,记住它的本质是数据响应。
Promise能做的Rx都能做,但是只要能用Promise的就不要用Rx。

Promise

基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Promise(function (resolve, reject) {
var timeOut = Math.random() * 2;
setTimeout(function () {
if (timeOut < 1) {
resolve('200 OK');
} else {
reject('error: ' + timeOut);
}
}, timeOut * 1000);
}).then(function (r) {
log('Done: ' + r);
}).catch(function (reason) {
log('Failed: ' + reason);
});

Promise的参数为一个方法有两个参数:resolvereject

  • resolve的参数可以在then中取到
  • reject的参数可以在catch中取到

串行执行

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
// 0.5秒后返回输入相乘的计算结果:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}

// 0.5秒后返回输入相加的计算结果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}

var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(2);
});

p.then(multiply)
.then(add)
.then(multiply)
.then(function (result) {
log('Got value: ' + result);
});

补充一点

1
setTimeout(resolve, 500, input);

相当于

1
2
3
setTimeout(function(){
resolve(input);
}, 500);

总结

链式调用的基础就是所有的方法的返回还是Promise对象

并行执行-同时获取

试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现

1
2
3
4
5
6
7
8
9
10
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});

并行执行-择优获取

多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

1
2
3
4
5
6
7
8
9
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});

由于p1执行较快,Promise的then()将获得结果'P1'p2仍在继续执行,但执行结果将被丢弃。
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。

Promise.reject

详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject

Promise.reject(reason)方法返回一个带有拒绝原因reason参数的Promise对象。

1
2
3
4
5
6
7
8
9
10
11
Promise.reject("Testing static reject").then(function(reason) {
// 未被调用
}, function(reason) {
console.log(reason); // "Testing static reject"
});

Promise.reject(new Error("fail")).then(function(result) {
// 未被调用
}, function(error) {
console.log(error); // stacktrace
});

Promise.resolve

详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。

value可传的值

  • 将被Promise对象解析的参数。
  • 一个Promise对象,
  • 一个thenable

如果该值为promise,返回这个promise;
如果这个值是thenable(即带有"then" 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;
否则返回的promise将以此值完成。
此函数将类promise对象的多层嵌套展平。

Resolve一个参数

示例

1
2
3
4
5
6
var promise1 = Promise.resolve(123);

promise1.then(function(value) {
console.log(value);
// expected output: 123
});

Resolve一个promise

示例

1
2
3
4
5
6
7
8
9
10
11
12
var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));

/*
* 打印顺序如下,这里有一个同步异步先后执行的区别
* original === cast ? true
* value: 33
*/

日志顺序颠倒其实是由于异步地调用then 方法。

Resolve一个thenable

不要在解析为自身的thenable 上调用Promise.resolve,这将导致无限递归,因为它试图展平无限嵌套的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
29
30
31
32
33
34
35
36
37
38
39
40
41
// Resolve一个thenable对象
var p1 = Promise.resolve({
then: function(onFulfill, onReject) { onFulfill("fulfilled!"); }
});
console.log(p1 instanceof Promise) // true, 这是一个Promise对象

p1.then(function(v) {
console.log(v); // 输出"fulfilled!"
}, function(e) {
// 不会被调用
});

// Thenable在callback之前抛出异常
// Promise rejects
var thenable = {
then: function(resolve) {
throw new TypeError("Throwing");
resolve("Resolving");
}
};

var p2 = Promise.resolve(thenable);
p2.then(function(v) {
// 不会被调用
}, function(e) {
console.log(e); // TypeError: Throwing
});

// Thenable在callback之后抛出异常
// Promise resolves
var thenable = { then: function(resolve) {
resolve("Resolving");
throw new TypeError("Throwing");
}};

var p3 = Promise.resolve(thenable);
p3.then(function(v) {
console.log(v); // 输出"Resolving"
}, function(e) {
// 不会被调用
});

async

当调用一个 async 函数时,会返回一个 Promise 对象。根据mdn的解释

  • 当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;
  • 当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。
  • async 函数中可能会有 await 表达式,await表达式会使 async 函数暂停执行,直到表达式中的 Promise 解析完成后继续执行 async中await后面的代码并返回解决结果。

注意: await 关键字仅仅在 async function中有效

例如:

1
2
3
4
5
6
7
8
async function testAsync() {
return "hello async";
}
let data = testAsync().then( (data) => {
console.log(data) // hello async
return data
});
console.log(data);

如果 async 函数没有返回值,又怎么样呢?很容易想到,它会返回 Promise.resolve(undefined)。

联想一下 Promise 的特点无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。

await

MDN的描述:

await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。
若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行async function。
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。
另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。

或者可以这样简单理解

await只能在async function中使用。
await把它后面的异步方法变成了同步方法,resolve函数参数作为await表达式的值。
await后的方法异常会抛出,所以外层要try/catch

async/await 相比原来的Promise的优势在于处理 then 链,不必把回调嵌套在then中,只要await即可,如

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
function say() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(`说话`);
}, 1000);
});
}
function sing() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(`唱歌`);
}, 1000);
});
}
async function mytest() {
try {
const v = await say();
const s = await sing();
console.log(v); // 说话
console.log(s) // 唱歌
} catch (e) {
console.log(e)
}
}
mytest();

延迟执行

1
2
3
4
5
6
7
8
9
10
async myfunc() {
console.info("延迟前执行");
let sleep = function (time) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
await sleep(800);
console.info("延迟后执行");
}