深入浅出 RxJS 之 函数响应式编程

2023-05-17 20:15:50 浏览数 (2)

# Hello RxJS

使用 jQuery 实现时间感应用。

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>
    <div>测试对时间的感觉</div>
    <button id="hold-me">按住 1s 后松开</button>
    <div>你的时间:<span id="hold-time"></span>ms</div>
    <div id="rank"></div>
  </div>
  <script
  src="https://code.jquery.com/jquery-1.12.4.js"
  integrity="sha256-Qw82 bXyGq6MydymqBxNPYTaUXXq7c8v3CwiYwLLNXU="
  crossorigin="anonymous"></script>
  <script>
    let startTime;
    $('#hold-me').mousedown(function() {
      startTime = new Date();
    });
    $('#hold-me').mouseup(function () {
      if (startTime) {
        const elapsedTime = new Date() - startTime;
        startTime = null;
        $('#hold-time').text(elapsedTime);
        $.ajax(`https://timing-sense-score-board.herokuapp.com/score/${elapsedTime}`)
          .done(function (data) {
            $('#rank').text(`你超过了 ${data.rank}% 的人`);
          });
      }
    });
  </script>
</body>
</html>

使用 RxJS 实现:

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>
    <div>测试对时间的感觉</div>
    <button id="hold-me">按住 1s 后松开</button>
    <div>你的时间:<span id="hold-time"></span>ms</div>
    <div id="rank"></div>
  </div>
  <script src="https://unpkg.com/rxjs@5.4.2/bundles/Rx.min.js"></script>
  <script>
    const holdMeBtn = document.querySelector('#hold-me');
    const mouseDown$ = Rx.Observable.fromEvent(holdMeBtn, 'mousedown');
    const mouseUp$ = Rx.Observable.fromEvent(holdMeBtn, 'mouseup');

    const holdTime$ = mouseUp$.timestamp().withLatestFrom(
      mouseDown$.timestamp(),
      (up, down) => up.timestamp - down.timestamp
    );

    holdTime$.subscribe(ms => {
      document.querySelector('#hold-time').innerText = ms;
    });

    holdTime$.flatMap(ms => Rx.Observable.ajax(`https://timing-sense-score-board.herokuapp.com/score/${ms}`))
      .map(e => e.response)
      .subscribe(data => {
        document.querySelector('#rank').innerText = `你超过了 ${data.rank}% 的人`;
      });
  </script>
</body>
</html>

RxJS 世界中有一种特殊对象——“流”,也可以叫“数据流”或“Observable对象”。

代表“流”的变量标识符,都是以 $ 结尾。

上面 mouseDown 和 mouseUp 都是数据流,分别代表按钮上的 mousedown 事件和 mouseup 事件集合,不光包含已经发生的事件,还包含没有发生的鼠标事件。对数据流一视同仁,这就是数据流的妙处。

“流”可以通过多种方法创造出来,mouseDown 和 mouseUp 通过 fromEvent 函数从网页的 DOM 元素中获得,holdTime 这个流则是通过 mouseDown 和 mouseUp

流对象中“流淌”的是数据,而通过 subscribe 函数可以添加函数对数据进行操作,上面的代码中,对 holdTime$ 对象有两个 subscribe 调用,一个用来更新 DOM,另一个用来调用 API 请求。

在 jQuery 的实现中,有被交叉访问的变量(startTime),两个不同函数的逻辑相互关联,稍有不慎就会引发 bug ,代码看起来就是一串指令的组合;在RxJS的代码中,没有这样纠缠不清的变量,会发现所有的变量其实都没有“变”,赋值时是什么值,就会一直保持这些值,代码是一个一个函数,每个函数只是对输入的参数做了响应,然后返回结果。

RxJS 引用了两个重要的编程思想:

  • 函数式
  • 响应式

# 函数式编程

# 什么是函数式编程

强调使用函数来解决问题的一种编程方式。函数式编程对函数的使用有一些特殊的要求,这些要求包括以下几点:

  • 声明式
  • 纯函数
  • 数据不可变性

从语言角度讲,JavaScript 不算一个纯粹意义上的函数式编程语言,但是,JavaScript 中的函数有第一公民的身份,因为函数本身就是一个对象,可以被赋值给一个变量,可以作为参数传递,由此可以很方便地应用函数式编程的许多思想。

把函数式编程看作一种编程思想,即使语言本身不支持一些特性,依然可以应用这样的编程思想,用于提高代码的质量。

JavaScript 如何满足函数式编程的特性需要:

声明式

命名式编程

代码语言:javascript复制
function double(arr) {
  const results = [];
  for (let i = 0; i < arr.length; i  ) {
    results.push(arr[i] * 2);
  }
  return results;
}

function addOne (arr) {
  const results = [];
  for (let i = 0; i < arr.length; i  ) {
    results.push(arr[i]   1);
  }
  return results;
}

  • 世界上很多问题都有相似的模式,命名式编程会使出现很多重复代码

声明式编程

把一个数组映射成另一个数组

代码语言:javascript复制
function double (arr) {
  return arr.map(function (item) {
    return item * 2;
  });
}
function addOne(arr) {
  return arr.map(function (item) {
    return item   1;
  });
}

进一步简化

代码语言:javascript复制
const double = arr => arr.map(item => item * 2);
const addOne = arr => arr.map(item => item   1);

纯函数

  • 纯函数要满足的条件
    • 函数的执行过程完全由输入参数决定,不会受除参数之外的任何数据影响
    • 函数不会修改任何外部状态,比如修改全局变量或传入的参数对象
  • 好处
    • 纯函数让代码更加简单,从而更加容易维护,更加不容易产生 bug
    • 非常容易写单元测试的
      • TDD 的难以推行很大原因是很多项目不遵守函数式编程规范
      • 如果被测函数都是纯函数,单元测试可以轻松达到 100% 的代码覆盖率。
  • 可能导致函数不纯的原因
    • 改变全局变量的值
    • 改变输入参数引用的对象
    • 读取用户输入,比如调用了 alert 或者 confirm 函数
    • 抛出一个异常
    • 网络输入/输出操作,比如通过 AJAX 调用一个服务器的 API
    • 操作浏览器的 DOM
  • 本质:做的事情是输入参数到返回结果的一个映射,不要产生副作用

数据不可变

  • 需要数据状态发生改变时,保持原有数据不变,产生一个新的数据来体现这种变化
  • 不可改变的数据就是 Immutable 数据,它一旦产生,就可以肯定它的值永远不会变,这非常有利于代码的理解

# 函数式编程和面向对象编程的比较

简单说来,面向对象的方法把状态的改变封装起来,以此达到让代码清晰的目的;而函数式编程则是尽量减少变化的部分,以此让代码逻辑更加清晰。

面向对象的思想是把数据封装在类的实例对象中,把数据藏起来,让外部不能直接操作这些对象,只能通过类提供的实例方法来读取和修改这些数据,这样就限制了对数据的访问方式。对于毫无节制任意修改数据的编程方式,面向对象无疑是巨大的进步,因为通过定义类的方法,可以控制对数据的操作。

但是,面向对象隐藏数据的特点,带来了一个先天的缺陷,就是数据的修改历史完全被隐藏了。有人说,面向对象编程提供了一种持续编写烂代码的方式,它让你通过一系列补丁来拼凑程序。

函数式编程中,倾向于数据就是数据,函数就是函数,函数可以处理数据,也是并不像面向对象的类概念一样把数据和函数封在一起,而是让每个函数都不要去修改原有数据(不可变性),而且通过产生新的数据来作为运算结果(纯函数)。

# 响应式编程

# Reactive Extension

Reactive Extension,也叫 ReactiveX,或者简称 Rx,指的是实践响应式编程的一套工具。

An API for asynchronous programming with observable streams.

Rx(包括RxJS)诞生的主要目的虽然是解决异步处理的问题,但并不表示 Rx 不适合同步的数据处理,实际上,使用 RxJS 之后大部分代码不需要关心自己是被同步执行还是异步执行,所以处理起来会更加简单。

# RxJS 是否是函数响应式编程

FRP 包含两个重要元素:

  • 指称性(denotative)
  • 临时的连续性(temporally continuous)

正统 FRP 认为,一个系统如果能被称为 FRP,除了要有 FunctionalReactive 的特点,还必须要能够支持两个事件可以“同时发生”,这就是指称性的要求。总之,按照正统 FRP 的说法,你的系统只有 FunctionalReactive,不能说自己是 FRP。

包括 RxJS 在内的 Rx,到底算不算 FRP ?按照正统 FRP 的观点,Rx 不算,因为 Rx 不满足指称性的要求,在 Rx 的所有实现中,都存在一个局限,就是当两个“流”合并的时候,不能按照 FRP 那样严格处理同时发生的事件。

# 函数响应式编程的优势

RxJS 模型的特点:

  • 数据流抽象了很多现实问题
    • 网页 DOM 的事件,可以看作为数据流
    • 通过 WebSocket 获得的服务器端推送消息可以看作是数据流
    • 通过 AJAX 获得服务器端的数据资源也可以看作是数据流,虽然这个数据流中可能只有一个数据
    • 网页的动画显示当然更可以看作是一个数据流
  • 擅长处理异步操作
    • 对数据采用“推”的处理方式,当一个数据产生的时候,被推送给对应的处理函数,这个处理函数不用关心数据是同步产生的还是异步产生的,这样就把开发者从命令式异步处理的枷锁中解放了出来
  • 把复杂问题分解成简单问题的组合
    • 数据流可能包含复杂的功能,但是可以分解成很多小的部分来实现,实现某一个小功能的函数就是操作符
    • 可以说,学习 RxJS 就是学习如何组合操作符来解决复杂问题

0 人点赞