RxJS 动画入门

Avatar of David Khourshid
David Khourshid

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200美元免费额度!

您可能之前听说过 RxJS、ReactiveX、响应式编程,甚至仅仅是函数式编程。在谈论最新的前端技术时,这些术语正变得越来越突出。如果您像我一样,在第一次尝试学习它时,一定会感到完全困惑。

根据 ReactiveX.io

ReactiveX 是一个库,用于通过使用可观察序列来组合异步和基于事件的程序。

在一个句子中消化这么多内容确实不容易。在本文中,我们将采用不同的方法来学习 RxJS(ReactiveX 的 JavaScript 实现)和 Observables,方法是创建**响应式动画**。

理解 Observables

数组是元素的集合,例如[1, 2, 3, 4, 5]。您可以立即获得所有元素,并且可以执行诸如mapfiltermap 等操作。这使您可以根据需要转换元素集合。

现在假设数组中的每个元素都随着时间推移出现;也就是说,您不会立即获得所有元素,而是一次获得一个。您可能会在 1 秒时获得第一个元素,在 3 秒时获得下一个元素,依此类推。以下是可能的表现形式

这可以描述为值流、事件序列,或者更相关地,**可观察对象**。

**可观察对象**是随时间推移的值的集合。

就像使用数组一样,您可以在这些值上执行 map、filter 等操作,以创建和组合新的可观察对象。最后,您可以订阅这些可观察对象,并对最终的值流执行任何操作。这就是 RxJS 发挥作用的地方。

RxJS 入门

使用 RxJS 最简单的方法是使用 CDN,尽管还有 许多安装方法,具体取决于您项目的需要。

<!-- the latest, minified version of RxJS -->
<script src="https://unpkg.com/@reactivex/rxjs@latest/dist/global/Rx.min.js"></script>

将 RxJS 引入项目后,您可以从几乎任何东西创建可观察对象

const aboutAnything = 42;

// From just about anything (single value).
// The observable emits that value, then completes.
const meaningOfLife$ = Rx.Observable.just(aboutAnything);

// From an array or iterable.
// The observable emits each item from the array, then completes.
const myNumber$ = Rx.Observable.from([1, 2, 3, 4, 5]);

// From a promise.
// The observable emits the result eventually, then completes (or errors).
const myData$ = Rx.Observable.fromPromise(fetch('http://example.com/users'));

// From an event.
// The observable continuously emits events from the event listener.
const mouseMove$ = Rx.Observable
  .fromEvent(document.documentElement, 'mousemove');

注意:变量末尾的美元符号 ($) 只是一个约定,表示该变量是一个可观察对象。可观察对象可用于建模任何可以表示为随时间推移的值流的事物,例如事件、Promises、计时器、间隔和动画。

就其本身而言,这些可观察对象并没有做太多事情,至少在您真正观察它们之前是这样。**订阅**将执行此操作,它是使用.subscribe() 创建的

// Whenever we receive a number from the observable,
// log it to the console.
myNumber$.subscribe(number => console.log(number));

// Result:
// > 1
// > 2
// > 3
// > 4
// > 5

让我们在实践中看看

查看笔

const docElm = document.documentElement;
const cardElm = document.querySelector('#card');
const titleElm = document.querySelector('#title');

const mouseMove$ = Rx.Observable
  .fromEvent(docElm, 'mousemove');

mouseMove$.subscribe(event => {
  titleElm.innerHTML = `${event.clientX}, ${event.clientY}`
});

mouseMove$ 可观察对象中,每次发生mousemove 事件时,订阅都会将titleElm.innerHTML 更改为鼠标的位置。.map 运算符(其工作方式类似于Array.prototype.map 方法)可以帮助简化操作

// Produces e.g., {x: 42, y: 100} instead of the entire event
const mouseMove$ = Rx.Observable
  .fromEvent(docElm, 'mousemove')
  .map(event => ({ x: event.clientX, y: event.clientY }));

通过一些数学运算和内联样式,您可以使卡片朝向鼠标旋转。pos.y / clientHeightpos.x / clientWidth 都计算出 0 到 1 之间的值,因此将其乘以 50 并减去一半 (25) 会产生 -25 到 25 之间的值,这正是我们旋转值所需的。

查看笔

const docElm = document.documentElement;
const cardElm = document.querySelector('#card');
const titleElm = document.querySelector('#title');

const { clientWidth, clientHeight } = docElm;

const mouseMove$ = Rx.Observable
  .fromEvent(docElm, 'mousemove')
  .map(event => ({ x: event.clientX, y: event.clientY }))

mouseMove$.subscribe(pos => {
  const rotX = (pos.y / clientHeight * -50) - 25;
  const rotY = (pos.x / clientWidth * 50) - 25;

  cardElm.style = `
    transform: rotateX(${rotX}deg) rotateY(${rotY}deg);
  `;
});

结合使用 .merge

现在假设您希望它响应鼠标移动或触摸移动(在触摸设备上)。无需任何回调混乱,您可以使用 RxJS 以多种方式组合可观察对象。在本例中,可以使用 .merge 运算符。就像多条车道合并成一条车道一样,这将返回一个包含来自多个可观察对象的合并的所有数据的单个可观察对象。

来源:http://rxmarbles.com/#merge
const touchMove$ = Rx.Observable
  .fromEvent(docElm, 'touchmove')
  .map(event => ({
    x: event.touches[0].clientX,
    y: event.touches[0].clientY
  }));

const move$ = Rx.Observable.merge(mouseMove$, touchMove$);

move$.subscribe(pos => {
  // ...
});

继续,尝试在触摸设备上平移

查看笔

还有其他 用于组合可观察对象的实用运算符,例如.switch().combineLatest().withLatestFrom(),我们将在接下来介绍。

添加平滑运动

旋转卡片虽然很整洁,但运动有点过于僵硬。每当鼠标(或手指)停止时,旋转会立即停止。为了解决这个问题,可以使用线性插值 (LERP)。Rachel Smith 在这篇很棒的教程中描述了通用技术。本质上,LERP 不会从点 A 跳到点 B,而是在每次动画滴答时前进一小部分。这会产生平滑的过渡,即使鼠标/触摸运动已停止。

让我们创建一个函数,它只有一个任务:使用 LERP 根据起始值和结束值计算下一个值

function lerp(start, end) {
  const dx = end.x - start.x;
  const dy = end.y - start.y;

  return {
    x: start.x + dx * 0.1,
    y: start.y + dy * 0.1,
  };
}

简短而甜蜜。我们有一个函数,每次都会返回一个新的线性插值位置值,方法是在每个动画帧上将当前(起始)位置向下一个(结束)位置移动 10%。

调度器和 .interval

问题是,我们如何在 RxJS 中表示动画帧?事实证明,RxJS 有一个名为**调度器**的东西,它控制可观察对象何时发出数据,以及其他一些事情,例如订阅何时应开始接收值。

使用 Rx.Observable.interval(),您可以创建一个可观察对象,该对象以定期安排的间隔发出值,例如每秒一次 (Rx.Observable.interval(1000))。如果您创建一个很小的间隔,例如Rx.Observable.interval(0) 并将其安排为仅在每个动画帧上使用Rx.Scheduler.animationFrame 发出值,则会在大约每 16 到 17 毫秒(在动画帧内)发出一个值,正如预期的那样。

const animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame);

结合使用 .withLatestFrom

要创建平滑的线性插值,您只需要关心每个动画滴答时最新的鼠标/触摸位置即可。为此,有一个名为 .withLatestFrom() 的运算符

const smoothMove$ = animationFrame$
  .withLatestFrom(move$, (frame, move) => move);

现在,smoothMove$ 是一个新的可观察对象,它仅在animationFrame$ 发出值时才发出来自move$ 的最新值。这是我们想要的——除非您真的很喜欢卡顿,否则您不希望在动画帧之外发出值。第二个参数是一个函数,用于描述在组合每个可观察对象的最新值时要执行的操作。在本例中,唯一重要的值是move 值,这也是返回的所有内容。

来源:http://rxmarbles.com/#withLatestFrom

结合使用 .scan 进行过渡

现在您有一个可观察对象,它在每个动画帧上发出来自move$ 的最新值,是时候添加线性插值了。.scan() 运算符根据一个函数(该函数接收这些值)“累积”可观察对象的当前值和下一个值。

来源:http://rxmarbles.com/#scan

这非常适合我们的线性插值用例。请记住,我们的lerp(start, end) 函数接受两个参数:start(当前)值和end(下一个)值。

const smoothMove$ = animationFrame$
  .withLatestFrom(move$, (frame, move) => move)
  .scan((current, next) => lerp(current, next));
  // or simplified: .scan(lerp)

现在,您可以订阅smoothMove$ 而不是move$ 以查看线性插值的效果。

查看笔

总结

当然,RxJS **并非**一个动画库,但以可组合、声明式的方式处理随时间变化的值是 ReactiveX 的核心概念,因此动画成为了演示该技术的一个绝佳方式。响应式编程是一种思考编程的不同方式,具有许多优势。

  • 它具有声明性、可组合性和不变性,避免了回调地狱,使您的代码更加简洁、可重用和模块化。
  • 它在处理各种异步数据方面非常有用,无论是获取数据、通过 WebSockets 通信、监听来自多个来源的外部事件,还是动画。
  • “关注点分离”——您使用 Observables 和操作符声明性地表示您期望的数据,然后在单个 .subscribe() 中处理副作用,而不是将它们散布在代码库中。
  • 它在**许多语言**中都有实现——Java、PHP、Python、Ruby、C#、Swift,以及您可能从未听说过的其他语言。
  • 它**不是一个框架**,并且许多流行的框架(如 React、Angular 和 Vue)与 RxJS 配合得非常好。
  • 如果您想装逼,可以这么说,但 ReactiveX 几乎在十年前(2009 年)首次实现,源于 Conal Elliott 和 Paul Hudak 二十年前(1997 年)的想法,在描述函数式反应式动画时(不出所料)。不用说,它是经受过考验的。

本文探讨了 RxJS 的许多有用部分和概念——使用 .fromEvent().interval() 创建 Observables,使用 .map().scan() 对 observables 进行操作,使用 .merge().withLatestFrom() 组合多个 observables,以及使用 Rx.Scheduler.animationFrame 引入调度器。还有许多其他有用的资源可以学习 RxJS。

如果您想更深入地了解使用 RxJS 进行动画制作(并使用 CSS 变量进行更声明式的操作),请查看 我在 CSS Dev Conf 2016 上的幻灯片我在 JSConf Iceland 2016 上的演讲,主题是使用 CSS 变量进行响应式动画。以下是一些使用 RxJS 进行动画制作的示例代码,可供参考。