RxJS(Reactive Extensions for JavaScript) 是一个非常强大的 JS 库,我们可以使用它轻松编写异步代码。
在本系列文章中,我将带领你学习 RxJS 的最新版本,我们会重点关注如何使用响应式编程范式来解决你在日常工作中碰到的问题。所以这是一个偏实战的系列文章。
在本系列文章中,你将学会 RxJS 中的核心组件是如何使用和运作的。
通过学习这个系列文章,你将使用 RxJS 完成一个完整的项目开发,在这个项目中,你将了解如何处理 DOM 事件、如何构建响应式本地数据库等内容。
响应式编程介绍
要学习 RxJS,首先要了解什么是响应式编程,我在这里对它进行一个简短的介绍,包括为什么需要它以及如何使用它。
为什么需要响应式编程范式?
如今的应用程序非常复杂,开发人员必须采用一些编程范式来编写代码,这些范式可以提供一定程度的抽象,来帮助我们处理复杂的业务逻辑。
在如今,几乎每个 Web App 都会产生大量的实时交互数据。当 App 的某一个部分发生变化时,可能会触发另外一个完全不同的部分一起发生变化。我们可以将这种行为视作一个“事件”。正确的处理这些事件,以保证 UI 是正确渲染的,这就是响应式编程范式试图解决的问题。
什么是响应式编程范式?
响应式编程的主要目的是为我们的代码提供一定程度的抽象,可以让我们专注于业务逻辑生成的事件,而不是处理大量的实现细节。
在很多内容类网站上,都会有一个点赞按钮,当我们点击这个按钮的时候,会改改变按钮的颜色为高亮色,并且将点赞数量加一。这些所有的事件都会在不阻塞 UI 渲染的情况下发生,从而实现流畅的用户交互体验和满意度。这只是一个很简单的例子,如今的每个 Web App 都会包含很多种事件的生成和响应。这就是响应式编程的最佳定义:以相同的方式对不同的事件做出相应,并在此过程中植入业务逻辑。
响应式编程已经通过 Rx-Libraries 实现了可用于多种语言和平台的一系列库。在本系列文章中,我们将重点关注 JavaScript 的实现库:RxJS,并深入探讨这种凡事如何使我们能够以一种从未想过的方式解决问题。通过编写和响应式编程范式息息相关的函数式编程代码,你可以远离传统的命令式编程。
本系列文章中讨论的响应式概念是通用的,可以通过使用不同的 Rx 库在任何语言或者框架中编写响应式编程的代码。
在正式学习响应式编程之前,你需要初步了解和异步相关的几个概念和问题,以及了解响应式编程是如何解决这些问题的。
JavaScript 中的异步编程
要了解响应式编程可以解决哪些问题,你必须首先了解 JavaScript 中执行异步任务的不同方式及其缺点。有了这些知识,你就会觉得响应式编程是执行异步任务的最佳方式。
首先,JavaScript 是一门单线程的语言,这意味着所有的逻辑都可以在单个线程中执行,如果某个逻辑包含了一个耗时很长的操作,比如访问 I/O 和 HTTP 调用,那么其他逻辑在等待这些操作执行完成之前,整个页面的 UI 都将保持阻塞状态。为了解决这个问题,JavaScript 引入了异步编程的概念。而解决异步任务最早的方案是回调函数,后来解决方案变成了 Promise,再后来,也就是现在,解决方案变成了 async/await。接下来,我们要将解决方案变成响应式编程。但在这之前,我们需要先来了解这些概念以及它们的优缺点。
回调函数 callback
为了确保我们的逻辑在长耗时的任务运行结束立即执行,最简单的办法就是将将要执行的逻辑封装成回调函数,传递给长耗时任务。
下面是一个点击鼠标的例子:
javascript
复制代码
constcallbackFn = evt => console.log('你点击了鼠标')
document.addEventListener('mousedown', callbackFn)
它的缺点如下:
- 容易形成回调地狱,嵌套层级过多,难以管理代码。
- 很难包含回调之间的依赖关系,比如需要一个接一个的请求多个 HTTP 请求。
Promise
使用 Promise,我么你可以更轻松的使用回调函数。
Promise 可以简单的理解为一个容器或者代理,用于在未来的某个时刻可用的对象。当未来的对象可用的时候,将会调用一个回调函数。Promise 带来了几个好处。可以观察下面这个将文件上传到服务器的示例:
const uploadSuccessHandler = evt => console.log('上传成功') const uploadFailedHandler = evt => console.log('上传失败') uploadFile(file, uploadSuccessHandler, uploadFailedHandler)
上面这段代码是使用回调函数编写的。uploadFile 函数会通过 HTTP 请求将文件发送到服务器,如果成功上传,会调用 uploadSuccessHandler,失败则会调用 uploadFailedHandler。
如果 uploadFile 函数中以 throw 的方式抛出 Error,uploadFailedHandler 函数将无法执行。
如果使用 Promise 来重写,代码如下:
const uploadSuccessHandler = evt => console.log('上传成功') const uploadFailedHandler = evt => console.log('上传失败') uploadFile(file) .then(uploadSuccessHandler) .catch(uploadFailedHandler)
then 表示在异步操作异步操作成功后的回调,catch 表示异步操作失败后的回调。在 catch 中,即使是 throw 的 Error,一样可以捕获。
另外我们还可以通过链式调用的方式完成多个异步操作。代码如下所示:
uploadFile(file) .then(uploadSuccessHandler) .then(uploadUIHandler) .then(uploadEmailHandler) .catch(uploadFailedHandler)
除此之外,Promise 还可以同时运行多个异步任务。使用 Promise.all 可以等待这些任务全部完成,使用 Promise.race 可以等待其中任意一个完成。
即使有了这些功能,Promise 也不是一个解决不同问题的灵丹妙药。使用 Promise 最常见的缺点如下:
- Promise 只能处理产生一个值的数据源。当某个数据源产生了多个值,Promise 就无法处理了,比如我们监听鼠标事件。
- 如果我们想要取消某一个长耗时的任务,例如某一个 HTTP 请求,Promise 无法提供这种功能。
但是,这些缺点不应该是阻止我们使用 Promise 的理由。Promise 是轻量级的 API,我们可以轻松的将它应用于单值数据源的场景中,我们也可以将 Promise 和 RxJS 组合,用于更复杂的场景中。