阅读 385

Rxjs vs Promise

这是我参与新手入门第三篇文章

既然有 Promise 为什么还需要 Rxjs 呢?

那 Rxjs 与 Promise 我们该如何选择呢?

这里有篇问答 What is the difference between Promises and Observables? ,已经介绍了一切。

在写 Angular 的时候,有时候需要返回 Promise。如果有时候拿到是 Promise,我们需要做其他处理,可以借助 Rxjs 强大的操作符特性,完成很多复杂的操作。Rxjs 与 Promise 相互转换技巧,需要大家一定要掌握,才能发挥它们的各自的优势。

本文重点是强调 Promises 和 Observables 的差异。这样做的目的是,如果你已经了解了 Promises(反之亦然),就可以更容易地理解 Observables。由于这个原因,我不在本文中讨论 RxJS 操作符,因为在 Promises 中没有任何东西可以与这些操作符相其并论。

JavaScript 中的异步编程

在说它们之前,我们不得不说 JavaScript 中的异步编程。

首先,让我们回顾一下 Promises 与 Observables 存在意义: 处理异步执行。

在 JavaScript 中有不同的方法来创建异步代码。其中最重要的是:

  • Callbacks
  • Promises
  • Async/Await
  • RxJS Observables

让我们简单地介绍一下它们。

Callbacks

这是异步编程的传统方法。提供一个函数作为另一个执行异步任务的函数的参数。当异步任务完成时,执行中的函数将调用回调函数。这种方法的主要缺点是当你有多个异步任务时,需要在回调函数中定义回调函数,我们把这种称为回调地狱

Promises

ES6(2015)中引入了 Promise,允许比回调更具有可读性的异步代码。

Callbacks 和 promise 之间的主要区别在于,使用 Callbacks 可以告诉执行函数异步任务完成时该做什么,而使用 promise 可以将执行函数返回给你一个特殊对象(promise),然后你可以告诉 promise 该做什么异步任务完成时。

const promise = asyncFunc();
promise.then(result => {
  console.log(result);
});
复制代码

也就是说,asyncFunc 立即向你返回一个 Promise,然后在异步任务完成时(通过它的.then方法)提供要采取的操作。

Async/Await

ES8(2017)中引入了 Async/Await。 该技术实际上应该列在 promise 下,因为它只是用于 promise 的语法糖。这种语法糖,确实值得研究。

基本上,你可以使用 async 一个函数声明为异步函数,从而可以在该函数的内部中使用 await 关键字。可以将 await 关键字放在计算为 promise 的表达式的前面。关键字 await 暂停异步功能的执行,直到 promise 返回 resolved。当这种情况发生时,整个 await 表达式计算为 promise 的结果值,然后继续执行异步函数。

此外,异步函数本身也会返回一个 promise,当函数体的执行完成时,promise 将返回 resolved。

function asyncTask(i) {
  return new Promise((resolve) => resolve(i + 1));
}
async function runAsyncTasks() {
  const res1 = await asyncTask(0);
  const res2 = await asyncTask(res1);
  const res3 = await asyncTask(res2);
  return "Everything done";
}
runAsyncTasks().then((result) => console.log(result));
复制代码

asyncTask 函数实现了一个异步任务,该任务接受一个参数并返回一个结果。该函数返回一个 Promise,异步任务完成后将解决该 Promise。这功能没有什么特别的,它只是一个返回 promise 的普通功能。

另一方面,将 runAsyncTasks 函数声明为 async,以便可以在其内部中使用 await 关键字。这个函数调用 asyncTask 三次,每次的参数必须是前面调用 asyncTask 的结果(即我们创建了三个异步任务)。

第一个 await 关键字导致 runAsyncTasks 的执行停止,直到 asyncTask(0) 返回的 promise 被解析为止。await asyncTask(0) 表达式然后计算解析 promise 的结果值,并分配给 res1。此时,asyncTask(res1) 被调用,第二个 await 关键字导致 runAsyncTasks 的执行再次停止,直到 asyncTask(res1) 返回的 promise 被解析。这种情况一直持续到最后 执行完 runAsyncTasks 函数体中的所有语句为止。

如前所述,async 函数本身会返回一个 promise,当函数内部执行完成时,promise 将使用函数的返回值进行解析。因此,换句话说,async 函数本身就是一个异步任务(它通常管理其他异步任务的执行)。这可以在最后一行看到,我们在返回的 promise 上调用 then 函数以打印出 async 函数的返回值。

如果我们 asyncTask 添加 log,那么打印出来的结果是:

res1
res2
res3
Everything done
复制代码

如果 async/await 只是 Promise 的语法糖,那么我们必须能够以纯粹的 Promise 实现上述示例:

function asyncTask(i) {
  return new Promise((resolve) => resolve(i + 1));
}
function runAsyncTasks() {
  return asyncTask(0)
    .then((res1) => {
      return asyncTask(res1);
    })
    .then((res2) => {
      return asyncTask(res2);
    })
    .then((res3) => {
      return "Everything done";
    });
}
runAsyncTasks().then((result) => console.log(result));
复制代码

这段代码等效于 async/await 版本,并且我们 runAsyncTasks 里的 then 打印 log,则它将产生与 async/await 版本相同的输出。

唯一改变的是 runAsyncTasks 函数。现在它是一个常规函数(而不是 async),它使用 thenasyncTask 返回的 promise(而不是 wait)。

相信大家不用我说,async/await 版本比 Promise 版本更具可读性和易懂性。实际上,async/await 的主要创新是允许以“看起来像”同步代码的 Promise 来编写异步代码。

RxJS Observables

首先,RxJS 是 ReactiveX 项目的 JavaScript 实现。ReactiveX 项目旨在为不同编程语言的异步编程提供 API。

ReactiveX 的基本概念是 Gang of Four 的 Observer pattern (ReactiveX 甚至通过完成和错误通知扩展了 observer 模式)。因此,所有 ReactiveX 实现的核心抽象是 observable。你可以在这里阅读更多关于 ReactiveX 的基本概念

那么,现在我们知道什么是 RxJS,但是什么是 observable 呢? 让我们尝试从两个维度来理解它,并将其与其他已知抽象进行比较。维度是同步性/异步性和单个值/多个值。

对于一个 observable,我们可以说以下是正确的:

  • 发送多个值
  • 异步发出其值(“推送”)

让我们将前面介绍的 promises 进行对比:

  • 发送单个值
  • 异步发出其值(“推送”)

最后,让我们看一看 Iterable,这是一种存在于许多编程语言中的抽象,可用于迭代集合数据结构(如数组)的所有元素。对于迭代,要满足以下条件成立:

  • 发送多个值
  • 同步发出其值(“拉取”)

注意:对于同步/拉异步/推,同步/拉意味着客户端代码从抽象中请求一个值并阻塞直到返回该值。异步/推送意味着抽象通知客户端代码正在发出新值,并且客户端代码处理此通知。

单个值多个值
同步GetIterable
异步PromiseObservable

注意:这里的 Get 仅代表常规的数据访问操作(例如常规函数调用)。

从上面的表格来看,我们可以说一个 Observable 到一个 Iterable 就等于一个 get 操作的 Promise。或者一个 promise 就像一个异步的 get 操作,而一个 observable 就像一个异步的 iterable。

我们也可以说,promise 和 Observable 之间的主要区别是,promise 只发出一个值,而 observable 发出多个值。

但是,让我们更详细地看一下。 通过简单的 get 操作(例如函数调用),调用代码将请求一个值,然后等待或阻塞,直到函数返回该值(调用代码提取该值)。

另一方面,有了 promise,调用代码也会请求一个值,但是直到返回该值时它才会阻塞。 它只是开始计算,然后继续执行自己的代码。 当 promise 完成值的计算时,它将值发送给调用代码,然后调用代码处理该值(将值推送到调用代码)。

现在,让我们来看 Iterable。在许多编程语言中,我们可以从集合数据结构(如数组)创建一个可迭代的对象。Iterable 通常有一个 next 方法,它从集合中返回下一个未读值。然后,调用代码可以重复调用 next 以读取集合的所有值。如上所述,每个 next 调用基本上都是一个同步阻塞 get 操作(调用代码反复提取值)。

Observable 将 Iterable 带到异步世界。像 Iterable 一样,Observable 会计算并发射流值。但是,与 Iterable 不同,对于 Observable,调用代码不会同步提取每个值,但 Observable 将异步地将每个值尽快推入调用代码。为此,调用代码为 Observable 提供了一个处理函数,然后在 RxJS 中调用该函数,然后 Observable 针对其计算的每个值调用此函数。

Observable 发出的值可以是任何东西:数组的元素,HTTP 请求的结果(如果 Observable 发出的只是一个值,不必总是多个值就可以),用户输入事件(例如鼠标单击等)。这使 Observable 非常灵活。 此外,由于 Observable 也只能发出单个值,因此 Observable 可以做 Promise 可以做的所有事情,但反之则不成立。

除此之外,ReactiveX Observable 还提供了大量所谓的 operators。这些功能可以应用于 Observable,以修改发射值集。

例如,我们可以配置一个 map 操作符如下: map(value => 2 * value),然后我们可以将这个操作符应用到一个 observable。结果是,observable 发出的每个值在被推送到调用代码之前都乘以 2。

import { Observable } from 'rxjs';
// 创建
const observable = new Observable(observer => {
  for (let i = 0; i < 3; i++) {
    observer.next(i);
  }
});
// 使用
observable.subscribe(value => console.log(value));
复制代码

到此,我们结束对 JavaScript 异步编程技术的概述。我们已经看到了 callbacks, promise 的 then 可以用于异步获取单个值,async/await 是 promise 的语法糖,而 RxJS observables 可以用于异步流获取值。

Promises 与 Observables 差异

我们将比较 Promises 与 Observables,并突出它们的差异。

安装

Promise 是 es6 的标准,如果想要在不支持 es6 的浏览器提供支持,需要引入polyfills即可。

Rxjs 不是标准,我们需要安装它才能使用。

你可以安装 RxJS 如下:

npm install --save rxjs@6
复制代码

你可以按照以下步骤在代码文件中导入 Observable 构造函数(这些示例所需的全部):

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
复制代码

但是,如果使用 Node.js,则必须按以下方式进行导入(因为 Node.js 尚不支持 import 语句):

const { Observable } = require('rxjs');
复制代码

注意:如果要运行以下包含 Observables 的代码示例,则必须安装和导入 RxJS 库。我们这里直接使用在线编辑器

创建

让我们看一下如何创建 Promise 与如何创建 Observable。 为简单起见,我们将首先忽略错误,仅考虑对 Promise 和 Observable 的“成功”执行。我们将在下一部分中介绍错误。

注意:promise 和 observables 都有两个方面:创建和使用。 一个 promise/observable 是一个首先需要由某人创建的对象。 创建之后,通常会将其传递给使用它的其他人。创建定义了 promise/observable 的行为以及发出的值,用法定义了对这些发出的值的处理。

一个典型的用例是,promise/observable 由 API 函数创建并返回给 API 用户。 然后,API 的用户将使用这些promise/observable 。 因此,如果你使用 API,则通常只使用promise/observable ,而如果你是 API 的作者,则还必须创建 promise/observable

在下面的内容中,我们将首先查看 promise/observable 的创建,并在随后的小节中介绍它们的用法。

Promises:

new Promise(executorFunc);
function executorFunc(resolve) {
  // Some code...
  resolve(value);
}
复制代码

要创建一个 Promise,你可以调用 Promise 构造函数,并将其传递给所谓的 executor 函数作为参数。 创建 promise 时,系统会调用 executor 函数,并将其作为参数传递给特殊的 resolve 函数(你可以根据需要命名此参数,只需记住 executor 函数的第一个参数是 resolve 函数,然后你必须按原样使用它)。

当你在 executor 函数的主体中调用 resolve 函数时,promise 将被转移到已完成状态,并将作为参数传递给 resolve 函数的值“发出”(promise 已解析)。

然后,这个发出的值将用作 onFulfilled 函数的参数,你将其作为 promise 的 then 函数的第一个参数传递给 promise 的用法方面的 then 函数,我们将在后面看到这一点。

Observables:

import { Observable } from 'rxjs';

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
}
复制代码

要创建一个 Observable,可以调用 Observable 构造函数,将其传递给所谓的 subscriber 函数作为参数。 每当新 subscriber 订阅 Observable 时,系统都会调用 subscriber 函数。subscriber 函数获取一个 Observable 作为参数。 该对象有一个 next 方法,该方法在被调用时会发出将其作为 Observable 的参数传递的值。

注意:在调用 next 之后,subscriber 函数将继续运行,并且可以多次调用 next。 这是与 Promise 的重要区别,Promise 在调用 resolve 之后,执行程序函数终止。Promise 最多可以发出一个值,而 Observable 则可以发出任意数量的值。

创建(带有错误处理)

上面的示例尚未显示出 Promise 和 Observables 的全部功能。 在执行 promise/observable 期间可能发生错误,并且两种技术都提供处理错误的手段。下面使用错误处理功能扩展上述解释。

Promises:

new Promise(executorFunc);
function executorFunc(resolve, reject) {
  // Some code...
  resolve(value); // 成功
  // Some code...
  reject(error); // 失败
}
复制代码

传递给 Promise 构造函数的执行程序函数实际上得到第二个参数,即 reject 函数。 reject 函数用于发送 Promise 执行中的错误。当调用它时,执行程序功能将中止,并且 Promise 将转移到 rejected 状态。

在使用方面,这将导致 onRejected 函数(可以将其传递给 catch 方法)被执行。

Observables:

import { Observable } from 'rxjs';

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value); // 成功
  // Some code...
  observer.error(error); // 失败
}
复制代码

作为参数传递给 subscriber 函数的 Observable 实际上还有另一种方法:error 方法。调用此方法向 Observable 的 subscriber 发送错误。

与 next 不同,调用 error 方法还会终止 subscriber 函数,从而终止 Observable。这意味着在一个 Observable 的生存期内,最多可以调用一次错误。

next 和 error 仍然不是全部。传递给 subscriber 函数的观察者对象还有另一种方法:complete。 其用法如下所示:

function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
  // If there is an error...
  observer.error(error);
  // If all successful...
  observer.complete();
}
复制代码

当 Observable 成功“完成”时,应该调用 complete 方法。完成意味着没有更多工作要做,也就是说,所有值都已发出。与 error 方法类似,complete 方法终止 subscriber 函数的执行,这意味着 complete 方法在 Observable 的生存期内最多可以调用一次。

建议调用 Observable 执行的 complete 方法,但不是必须的。

使用

在介绍了 promise 和 observable 的创建之后,现在让我们看一下它们的用法。使用 promise 或 observable 意味着“订阅”它,这又意味着向将为每个发射值(promise 的一个值,observable 的任何数量的值)调用的 promise 或 observable 注册处理程序函数。

处理函数的注册是通过 promise 或 observable 对象的特殊方法完成的。这些方法分别是:

在下面的内容中,我们展示了这些方法在 promise 和 observables 中的基本用法。同样,我们将首先考虑忽略错误处理的基本情况,然后在下一个小节中添加错误处理。

注意:在以下代码段中,我们假定一个 promise 或 Observable 对象已经存在。 因此,如果要运行代码,则必须在前面添加一个 promise 或 Observable 的创建语句,例如:

  • const promise = new Promise();
  • const observable = new Observable();

Promises:

promise.then(onFulfilled);
function onFulfilled(value) {
  // Do something with value...
}
复制代码

给定一个 promise 对象,我们调用该对象的 then 方法,并将其传递给 onFulfilled 函数作为参数。 onFulfilled 函数采用单个参数。 此参数是 promise 的结果值,即已传递给 promise 中的 resolve 函数的值。

Observables:

使用 Observable 的方式订阅它,这是通过 Observable 的订阅方法完成的。 实际上,有两种等效的方式可以使用 subscription 方法。 在下面,我们将介绍它们两者:

第一种:

observable.subscribe(nextFunc);
function nextFunc(value) {
  // Do something with value...
}
复制代码

在这种情况下,我们调用 Observable 的 subscription 方法,并将其 next 函数作为参数传递给它。 next 函数采用单个参数。 只要 Observable 发出值,则此参数为当前发出的值。

换句话说,只要 Observable 的内部 subscriber 函数调用 next 方法,就会使用传递给 next 的值来调用你的 next 函数(从而将值从 Observable 传递到你的处理函数)。

第二种:

observable.subscribe({
  next: nextFunc,
});
function nextFunc(value) {
  // Do something with value...
  console.log(value);
}
复制代码

第二种选择可能看起来有点奇怪,但实际上它更好地显示了在幕后发生的事情。

在这种情况下,我们不使用功能作为参数而是使用对象来调用 subscribe 。 该对象具有单个属性,该属性具有称为 next 的键和函数值。 该功能不过是我们上面很好的 next 功能。

其他所有内容保持不变,我们只是将 next 函数传递给对象内部,而不是直接作为参数。但是,为什么在将处理函数传递给 subscribe 方法之前将其包装在一个对象中呢?

可以通过这种方式 subscribe 的对象实际上是实现 Observer 接口的对象。也许你还记得当我们在前面的小节中创建 Observable 时,我们曾经定义了一个 subscriber 函数,并且该 subscriber 函数采用一个称为 observer 的参数。我们使用了如下代码:

new Observable(subscriberFunc);
function subscriberFunc(observer) {
  // Some code...
  observer.next(value);
}
复制代码

subscriber 函数的 observer 参数直接对应于我们上面传递给 subscribe 的对象(实际上,传递给订阅的对象首先从类型 Observer 转换为 subscriber ,然后再传递给 subscriber 函数,并且 Subscriber 实现 Observer 接口)。

因此,在第二种选择,我们已经创建了一个对象,该对象构成了将传递到 Observable 的 Subscriber 函数中的实际对象的基础,而在第一种选择,我们仅提供了将用作该对象方法的函数。

使用这两个选项中的哪一个取决于自己喜好。注意:如果使用第二种选择,则必须强制调用 next 函数的对象属性键。 这由该对象需要实现的 Observer 接口规定。

使用(带错误处理)

和创建一样,我们现在将使用示例扩展为包括错误处理。在这种情况下,错误处理意味着提供一个特殊的处理函数来处理由 promise 或 observable 表示的潜在错误(除了处理 promise 或 observable 发出的“有规律的”值的“有规律的”处理函数之外)。

对于 promise 和 observable,在两种情况下都可能产生错误:

  1. promise 或 observable 实现分别调用 reject 函数或 error 方法(见创建错误)。
  2. promise 或 observable 实现抛出一个带有 throw 关键字的错误。

让我们看看如何处理 promise 和 observable 值的这些类型的错误:

Promises:

实际上有两种方法来处理 promise 发出的错误。第一个使用 then 方法的第二个参数,第二个使用 catch 方法,下面我们将介绍这两种方法。

选择一(then 的第二个参数):

promise.then(onFulfilled, onRejected);
function onFulfilled(value) {
  // Do something with value...
}
function onRejected(error) {
  // Do something with error...
}
复制代码

promise 的 then 方法采用第二个函数参数,即 onRejected 函数。当 promise 的 executor 函数调用 reject 函数时,或者当 promise 的 executor 函数抛出带有 throw 关键字的错误时,将调用此函数。

提供 onRejected 函数可以处理此类错误。如果你不提供它,那么仍然可能发生错误,但是你的代码不能处理这些错误。

选择二(catch 方法):

promise.then(onFulfilled).catch(onRejected);
function onFulfilled(value) {
  // Do something with value...
}
function onRejected(error) {
  // Do something with error...
}
复制代码

也就是说,我们不同时向 then 方法提供 onFulfilled 和 onRejected 函数,而是只向 then 提供 onFulfilled 方法,然后调用 then 返回的 promise 的 catch 方法,并将 onRejected 函数传递给该 catch 方法。注意,在这种情况下,我们调用 catch 的 promise(和返回 then)与初始 promise 相同。

实际上,使用 catch 方法的第二个选项比第一个选项更常见。它利用了 Promise 的重要链式方法。对 Promise 链式方法的讨论不在本文讨论范围之内,如需了解看这里

关于链式方法,需要注意的重要一点是 then 和 catch 总是返回一个 promise,它允许在同一语句中重复调用这些方法,如上所示。返回的 promise 与先前的 promise 相同,或者是一个新的 promise。后一种情况允许在不使用任何嵌套形式的情况下直接处理嵌套异步任务(如果使用回调将导致回调地狱)。顺便说一下,这是 promise 优于回调的主要优点之一。

另一点值得注意的是,catch 实际上并没有什么特别之处。事实上,catch 方法只是对 then 方法的特定调用的语法糖。特别是,使用 onRejected 函数作为惟一的参数调用 catch 与使用未定义的第一个参数调用 catch、使用 onRejected 作为第二个参数调用 catch 是等价的。

因此,以下两种说法是等价的:

promise.then(onFulfilled).catch(onRejected);
promise.then(onFulfilled).then(undefined, onRejected);
复制代码

因此,我们可以从概念上减少 then 的链简化为纯粹的 then 的纯链,这样有时就更容易对它们进行理解。

Observables:

正如在创建中已经提到的,有两种方式来调用一个 Observable 的 subscribe 方法。一个使用对象(实现 Observer)作为参数,另一个使用函数作为参数。

我们将介绍这两种方式:

选择一(函数参数):

observable.subscribe(nextFunc, errorFunc);
function nextFunc(value) {
  // Do something with value...
}
function errorFunc(error) {
  // Do something with error...
}
复制代码

与使用中没有错误处理的情况相比,惟一的区别是我们向 subscribe 方法传递了第二个函数参数。第二个参数是 error 函数,当 Observable 的 subscribe 函数调用其传递的 Observer 参数的 error 方法时,或者抛出一个带有 throw 的错误时,都会调用这个错误函数。

选择二(对象参数):

observable.subscribe({
  next: nextFunc,
  error: errorFunc,
});
function nextFunc(value) {
  // Do something with value...
}
function errorFunc(error) {
  // Do something with error...
}
复制代码

这里与没有错误处理的情况的惟一区别是我们传递给 subscribe 方法的对象中的添加了 error 属性。此属性的值是 error 处理函数。

实际上还有第三个函数可以传递给 subscribe 方法:complete(我们在前面的已经提到过)。此函数可以作为要 subscribe 的第三个参数(函数参数)传递,也可以作为传递给 subscribe 的 Observer 中添加 complete 属性传递(对象参数)。此属性的值是 complete 处理函数。

此外,这三个函数的每个规范都是可选的。如果你不提供它,那么将不会对相应的事件执行任何操作。总之,这为你提供了以下调用 subscribe 的方法:

  1. 如果是函数参数:可以传递一个,两个或三个函数。
  2. 如果是对象参数:包含 next,error 和 complete 的可选函数属性。

创建+使用:示例

我们将应用所有概念来一些完整的示例,并在一个实际示例中使用 promise 和 observables 来实现。

我们将在在线编辑器里展示这些栗子

Promises:

// 创建
const promise = new Promise(executorFunc);
function executorFunc(resolve, reject) {
  const value = Math.random();
  if (value <= 1 / 3) {
    resolve(value);
  } else if (value <= 2 / 3) {
    reject('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
}
// 使用
promise.then(onFulfilled).catch(onRejected);
function onFulfilled(value) {
  console.log('Got value: ' + value);
}
function onRejected(error) {
  console.log('Caught error: ' + error);
}
复制代码

这段代码创建一个 Promise,生成 0 到 1 之间的随机数。如果数字小于或等于 1/3,则使用此值解析 Promise(该值“发出”)。如果数字大于 1/3 但小于或等于 2/3,则 Promise 被拒绝。最后,如果该数字大于 2/3,则使用 JavaScript throw 关键字抛出一个错误。

这个程序有三种可能的输出:

// log1:
// Got value: 0.2109261758959049
// log2:
// Caught error: Value <= 2/3 (reject)
// log3:
// Caught error: Value > 2/3 (throw)
复制代码

当解析 promise(使用 resolve 函数)时,输出 log1 发生。这将导致使用解析值执行 onFulfilled 处理函数。

当 promise 被明确拒绝(使用拒绝函数)时,输出 log2 发生。这将导致执行 onRejected 处理函数。

最后,当 promise 执行过程中抛出错误时,输出 log3 发生。与明确拒绝 promise 一样,这将导致执行 onRejected 处理函数。

在上面的代码中,我们使用了相对冗长的语法,因为我们使用了命名函数。使用匿名函数是很常见的,这使得代码更加简洁。在这方面,我们可以将上面的代码等价地重写如下

// 创建
const promise = new Promise((resolve, reject) => {
  const value = Math.random();
  if (value <= 1 / 3) {
    resolve(value);
  } else if (value <= 2 / 3) {
    reject('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
});
// 使用
promise.then(value => console.log('Got value: ' + value)).catch(error => console.log('Caught error: ' + error));
复制代码

Observables:

import { Observable } from 'rxjs';

// 创建
const observable = new Observable(subscriberFunc);
function subscriberFunc(observer) {
  const value = Math.random();
  if (value <= 1 / 3) {
    observer.next(value);
  } else if (value <= 2 / 3) {
    observer.error('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
  observer.complete();
}
// 使用
observable.subscribe(nextFunc, errorFunc, completeFunc);
function nextFunc(value) {
  console.log('Got value: ' + value);
}
function errorFunc(error) {
  console.log('Caught error: ' + error);
}
function completeFunc() {
  console.log('Completed');
}
复制代码

这个例子和上面的 Promises 是一样的。如果随机值小于或等于 1/3,则被 Observable 用传递的 observer 的 next 方法发出值。如果该值大于 1/3,但小于或等于 2/3,则用 observer 的 error 方法表示错误。最后,如果值大于 2/3,则抛出带有 throw 关键字的错误。在 subscriber 函数的末尾,调用 observer 的 complete 方法。

这个程序有三种可能的输出:

// log1:
// Got value: 0.2109261758959049
// Completed
// log2:
// Caught error: Value <= 2/3 (reject)
// log3:
// Caught error: Value > 2/3 (throw)
复制代码

当从 Observable 发出有规律的值时,将发生输出 log1。它导致 nextFunc 处理函数被执行。因为 observable 的 subscribe 函数也会在其内部的最后调用 complete,所以 completeFunc 处理函数也会被执行。

当 Observable 调用 observer 的 error 方法时,将发生输出 log2。这将导致 errorFunc 处理函数被执行。注意,这也会导致 observable 的 subscribe 函数的执行被中止。因此,不会调用 subscriber 函数内部的 complete 方法,这意味着也不会执行 completeFunc 处理函数。你可以看到这一点,因为输出 log1 中没有完整的输出行。

如果 observable 的 subscriber 函数使用 throw 关键字引发错误,则输出 log3 发生。它具有与调用 error 方法相同的效果,即执行 errorFunc 处理函数,并且中止 observable 的 subscriber 函数的执行(不调用 complete 方法)。

我们可以用一个更简洁的符号来重写这个例子:

import { Observable } from 'rxjs';

// 创建
const observable = new Observable(observer => {
  const value = Math.random();
  if (value <= 1 / 3) {
    observer.next(value);
  } else if (value <= 2 / 3) {
    observer.error('Value <= 2/3 (reject)');
  } else {
    throw 'Value > 2/3 (throw)';
  }
  observer.complete();
});
// 使用
observable.subscribe({
  next(value) {
    console.log('Got value: ' + value);
  },
  error(err) {
    console.log('Caught error: ' + err);
  },
  complete() {
    console.log('Completed');
  },
});
复制代码

请注意,这里我们使用了 subscribe 方法的另一种用法,该方法将单个对象作为参数,并将处理程序函数作为其属性。另一种方法是使用带有三个匿名函数的 subscribe 方法作为参数,但是在一个参数列表中有多个匿名函数通常是不方便的和不可读的。然而,这两种用法是完全相同的,你可以选择任何你想要的。

到目前为止,我们比较了 promise 和 observables 的创建和使用。接下来,我们将研究 promise 和 observables 之间的一系列其他差异。

单个值与多个值

  • promises 只能发出单个值。之后,它处于完成状态,只能用于查询该值,而不能再用于计算和发出新值。
  • observables 可以发出任意数量的值。

Promises:

const promise = new Promise(resolve => {
  resolve(1);
  resolve(2);
  resolve(3);
});
promise.then(result => console.log(result));
// logs:
// 1
复制代码

只执行 executor 函数中解析的第一个 resolve 调用,并使用值 1 解析 promise。之后,promise 转移到完成状态,结果值不再变化。

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
});
observable.subscribe(result => console.log(result));
// logs:
// 1
// 2
// 3
复制代码

subscriber 函数中对 observer.next 的每次调用都会生效,发出一个值并执行该处理函数。

预解析与惰性的

  • promise 是预解析:一旦创建了 promise,就会调用执行程序函数 Executor。
  • Observable 是惰性的:仅当客户端订阅 Observable 时才调用 subscriber 函数。

Promises:

const promise = new Promise(resolve => {
  console.log('- Executing');
  resolve();
});
console.log('- Subscribing');
promise.then(() => console.log('- Handling result'));
// logs:
// - Executing
// - Subscribing
// - Handling result
复制代码

可以看到,在订阅 promise 之前,executor 函数已经执行了。

如果根本没有订阅承诺,甚至会执行 executor 函数。如果注释掉最后两行,就可以看到这一点:仍然输出- Executing

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer => {
  console.log('- Executing');
  observer.next();
});
console.log('- Subscribing');
observable.subscribe(() => console.log('- Handling result'));
// logs:
// - Subscribing
// - Executing
// - Handling result
复制代码

正如我们所看到的,subscriber 函数只在创建了对 Observable 的订阅之后执行。

如果将最后两行注释掉,则根本没有输出,因为 subscriber 函数将永远不会执行。

由于 Observable 不是在定义时执行的,而是在其他代码使用它们时才执行,所以它们也称为声明式(声明一个 Observable,但仅在使用它时才执行)。

取消订阅

  • 一旦使用 then 订阅了一个 promise,无论如何,传递给 then 的处理函数都将被调用。一旦 promise 执行开始,就不能告诉 promise 取消调用结果处理函数。
  • 在使用 subscribe 订阅一个 Observable 之后,可以通过调用 subscribe 返回的订阅对象的 unsubscribe 方法,随时取消订阅。

Promises:

const promise = new Promise(resolve => {
  setTimeout(() => {
    console.log('Async task done');
    resolve();
  }, 2000);
});
// 不能再阻止handler被执行了。
promise.then(() => console.log('Handler'));
// logs:
// Async task done
// Handler
复制代码

一旦我们调用了 then,就无法阻止调用传递给 then 的处理函数(即使我们有 2 秒的时间)。因此,2 秒后,当 promise 被解析时,处理程序就会执行。

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer => {
  setTimeout(() => {
    console.log('Async task done');
    observer.next();
  }, 2000);
});
const subscription = observable.subscribe(() => console.log('Handler'));
subscription.unsubscribe();
// logs:
// Async task done
复制代码

我们订阅了 observable,向它注册了一个处理函数,但是紧接着我们又从 observable 中取消订阅。其结果是,2 秒后,当 observable 将发出它的值时,我们的处理函数不会被调用。

注意:仍会打印完成的异步任务。取消订阅本身并不意味着 observable 正在执行的任何异步任务都将中止。取消订阅只是实现了对 subscriber 函数中对 observer.next(以及 observer.error 和 observer.complete)的调用不会触发对处理函数的调用。但是其他所有内容仍然可以正常运行,就好像不会取消订阅一样。

多播和单播

  • promise 的 executor 函数只执行一次(在创建 promise 时)。这意味着,对给定 promise 对象的所有调用都直接进入 executor 函数的正在执行,最后得到结果是值的副本。因此,promise 执行多播,是因为相同的执行和结果值用于多个订阅者。
  • observable 的 subscriber 函数在每个调用上执行以订阅该 observable。 因此,可观察对象执行单播,因为每个订阅服务器有单独的执行和结果值。

Promises:

const promise = new Promise(resolve => {
  console.log('Executing...');
  resolve(Math.random());
});
promise.then(result => console.log(result));
promise.then(result => console.log(result));
// logs:
// Executing...
// 0.1277775033205002
// 0.1277775033205002
复制代码

可以看到,executor 函数只执行一次,并且两个订阅之间共享结果值。

Observables:

import { Observable } from 'rxjs';

const observable = new Observable(observer => {
  console.log('Executing...');
  observer.next(Math.random());
});
observable.subscribe(result => console.log(result));
observable.subscribe(result => console.log(result));
// logs:
// Executing...
// 0.9823994838399746
// Executing...
// 0.8877532356021958
复制代码

可以看到,subscriber 函数是为每个 subscriber 单独执行的,每个 subscriber 都有自己的结果值。

异步执行与同步执行

  • promise 的处理函数是异步执行的。也就是说,它们是在执行完主程序或当前功能中的所有代码之后执行的。
  • observable 的处理函数是同步执行的。也就是说,它们是在当前函数或主程序流中执行的。

Promises:

console.log('- Creating promise');
const promise = new Promise(resolve => {
  console.log('- Promise running');
  resolve(1);
});
console.log('- Registering handler');
promise.then(result => console.log('- Handling result: ' + result));
console.log('- Exiting main');
// logs:
// - Creating promise
// - Promise running
// - Registering handler
// - Exiting main
// - Handling result: 1
复制代码

首先创建 promise,然后直接执行 promise(因为 promise 的 executor 函数预先的,请参见上文)。承诺也立即得到解决。之后,我们通过调用 promise 的 then 方法来注册一个处理函数。至此,promise 已经被解析(即它处于已完成状态),然而,我们的处理程序函数此时尚未执行。而是首先执行主程序中所有剩余的代码,然后再调用我们的处理函数。

原因是 promise 完成(或拒绝)是作为异步事件处理的。这意味着,当一个承诺被解析(或拒绝)时,相应的处理函数将作为单独的项放在 JavaScript 事件队列中。这意味着处理程序仅在事件队列中的所有先前项目均已执行后才执行,并且在我们的示例中,有一个此类先前项目即主程序。

Observables:

import { Observable } from 'rxjs';

console.log('- Creating observable');
const observable = new Observable(observer => {
  console.log('- Observable running');
  observer.next(1);
});
console.log('- Registering handler');
observable.subscribe(v => console.log('- Handling result: ' + v));
console.log('- Exiting main');
// logs:
// - Creating observable
// - Registering handler
// - Observable running
// - Handling result: 1
// - Exiting main
复制代码

首先,创建了 Observable(但是它还没有被执行,因为 Observable 是惰性的,请参见上文),然后我们通过调用 Observable 的 subscribe 方法注册一个处理函数。这时,observable 开始运行,并立即发出它的第一个也是唯一一个值。现在执行了处理函数,主程序退出。

与 promise 不同,处理函数是在主程序仍在运行时运行的。这是因为 observable 的处理函数是在当前执行的代码中同步调用的,而不是像 promise 的处理函数那样作为异步事件调用的。

我们从创建,使用,发送数据,销毁,执行方式多方便研究了 Promises 与 Observables 差异,你会发现 Observables 在各方便都优于 Promises,是不是 Promises 真的不行了,答案:不是。使用场景不一样,我们使用技术也不一样,Promises 有一个杀手锏 async/await

Promise 和 RxJS Observable 互相操作

Rxjs 有很多操作符,我们介绍一些常用操作符可以直接把 Promise 转 Observable:

  • of
  • from
  • defer
  • forkJoin
  • concatMap
  • mergeMap
  • switchMap
  • exhaustMap
  • bufferToggle
  • audit
  • debounce
  • throttle
  • scheduled

Observable 转 Promise 只有两个方法 toPromiseforEach

前面我们说async/await是 Promise 法宝,但是async/await和 Observables 不能真正“协同工作”,我们可以借助 Observable 与 Promises 高度的互操作性来完成。

如果接受 Observable,则接受 Promise

我们上面列举很多操作符都可以将 Promise 转 Observable。

例如,如果正在使用一个 switchMap,可以在其中返回一个 Promise,就像可以返回一个 Observable 一样。所有这些都是有效的:

import { interval, of } from 'rxjs';
import { mergeAll, take, map, switchMap } from 'rxjs/operators';

// 每1秒发射100的10倍的可观测值
const source$ = interval(1000).pipe(
  take(10),
  map(x => x * 100),
);
/**
 * 返回一个承诺,等待“ms”毫秒并发出“done”
 */
function promiseDelay(ms) {
  return new Promise(resolve => {
    setTimeout(() => resolve('done'), ms);
  });
}

// 使用 switchMap
source$
  .pipe(switchMap(x => promiseDelay(x))) // 回调
  .subscribe(x => console.log('switchMap1', x));

source$
  .pipe(switchMap(promiseDelay)) // 再简洁一点
  .subscribe(x => console.log('switchMap2', x));

// 或者是你想做的奇怪的事情
of(promiseDelay(100), promiseDelay(10000))
  .pipe(mergeAll())
  .subscribe(x => console.log('of', x));
复制代码

如果可以访问创建承诺的函数,那么可以使用 defer() 将其封装起来,并创建一个可在错误时重试的 Observable。

import { defer } from 'rxjs';
import { retry } from 'rxjs/operators';

function getErringPromise() {
  console.log('getErringPromise called');
  return Promise.reject(new Error('sad'));
}

defer(getErringPromise)
  .pipe(retry(3))
  .subscribe(x => console.log);
// logs
// getErringPromise called
// getErringPromise called
// getErringPromise called
// Error: sad
复制代码

事实证明,Defer 是一个非常强大的操作符。你可以将其基本上直接与 async/await 函数一起使用,它将使 Observable 发出返回的值并完成。

import { defer } from 'rxjs';

function promiseDelay(ms) {
  return new Promise(resolve => {
    setTimeout(() => resolve('done'), ms);
  });
}

defer(async function() {
  const a = await promiseDelay(1000).then(() => 1);
  const b = a + (await promiseDelay(1000).then(() => 2));
  return a + b + (await promiseDelay(1000).then(() => 3));
}).subscribe(x => console.log(x));
// logs:
// 7
复制代码

订阅 Observable 的方法不止一种,有一个 subscribe,这是经典的订阅 Observable 的方式,它返回一个 Subscription 对象,该对象可用于取消订阅,还有 forEach,这是一种不可撤销的订阅 Observable 的方式,该方式需要一个函数每个 next 值,并返回一个 Promise,其中包含 Observablecompleteerror

import { fromEvent } from 'rxjs';
import { take } from 'rxjs/operators';

const click$ = fromEvent(document.body, 'click');
function promiseDelay(ms) {
  return new Promise(resolve => {
    setTimeout(() => resolve('done'), ms);
  });
}

/**
 * 等待5次点击
 * 点击完成等待promiseDelay执行
 */
async function doWork() {
  await click$.pipe(take(5)).forEach(i => console.log(`click ${i}`));
  return await promiseDelay(1000);
}

doWork().then(v => console.log(v));

// logs:
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// click [object MouseEvent]
// done
复制代码

toPromise 函数跟 forEach 一样,是 Observable 上的方法,订阅一个 Observable 并将其封装到一个 Promise 中的方法。Promise 将在 Observable 完成后解析为 Observable 的最后一个被释放的值。如果 Observable 永远不会完成,那么 Promise 永远不会解析。

import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

const source$ = interval(1000).pipe(take(3)); // 0, 1, 2
async function test() {
  return await source$.toPromise();
}

test().then(v => console.log(v));
// logs:
// 2
复制代码

注意:使用 toPromise() 是一种反模式,除非你正在处理一个期望 Promise 的 API,比如async/await。Rxjs v7 版本弃用 toPromise()

forEachtoPromise 虽然都是返回 Promise 表现却不一致。

存在即合理,技术没有好坏,我们需要扬长避短,组合使用,发挥技术的最大优势,写出我们最健壮的程序。

今天就到这里吧,伙计们,玩得开心,祝你好运!

喜欢这篇文章吗?如果是的话,欢迎给我一个 ⭐。

文章分类
前端
文章标签