您可能之前听说过 RxJS、ReactiveX、响应式编程,甚至仅仅是函数式编程。在谈论最新的前端技术时,这些术语正变得越来越突出。如果您像我一样,在第一次尝试学习它时,一定会感到完全困惑。
根据 ReactiveX.io
ReactiveX 是一个库,用于通过使用可观察序列来组合异步和基于事件的程序。
在一个句子中消化这么多内容确实不容易。在本文中,我们将采用不同的方法来学习 RxJS(ReactiveX 的 JavaScript 实现)和 Observables,方法是创建**响应式动画**。
理解 Observables
数组是元素的集合,例如[1, 2, 3, 4, 5]
。您可以立即获得所有元素,并且可以执行诸如map、filter 和map 等操作。这使您可以根据需要转换元素集合。
现在假设数组中的每个元素都随着时间推移出现;也就是说,您不会立即获得所有元素,而是一次获得一个。您可能会在 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 / clientHeight
和pos.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
运算符。就像多条车道合并成一条车道一样,这将返回一个包含来自多个可观察对象的合并的所有数据的单个可观察对象。

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
值,这也是返回的所有内容。

.scan
进行过渡
结合使用 现在您有一个可观察对象,它在每个动画帧上发出来自move$
的最新值,是时候添加线性插值了。.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。
- ReactiveX: RxJS – 官方文档
- RxMarbles – 用于可视化 observables
- 你一直错过的响应式编程入门 by Andre Staltz
如果您想更深入地了解使用 RxJS 进行动画制作(并使用 CSS 变量进行更声明式的操作),请查看 我在 CSS Dev Conf 2016 上的幻灯片 和 我在 JSConf Iceland 2016 上的演讲,主题是使用 CSS 变量进行响应式动画。以下是一些使用 RxJS 进行动画制作的示例代码,可供参考。
精彩的文章,谢谢。如何一步步构建/调试响应式代码?
他们为 RxMarbles 做了很棒的工作,这是一个很好的切实例子,服务于真实的用例。谢谢!
一直想学习 RxJS,但没有机会。这是一个很棒的入门介绍!
对于平滑示例,
smoothMove$
发出的值是否永远不会真正达到最新值,至少在理论上是这样?对于每个动画步骤,最后一个发出的值都会向最后一个实际值靠近 10%,这意味着实际值永远不会被发出。当然,在有限的精度下,以及根据它的用途,这并不是真正的问题。只是一个有趣的思考。
很棒的文章!我非常喜欢这些例子。代表 RxJS 团队,感谢您撰写这篇文章。
谢谢,Ben!
哇,太棒了。
我学习了
animationFream$
和lerp
。谢谢 +1希望有更多关于 RxJS 和动画的教程 :)
解释和示例都很棒的文章!
恕我直言,您在这里描述的观察者模式等同于原生事件驱动方法。
RxJS 的优点是使用 Ecmascript6 数组方法来生成更简洁的代码。否则,这缺少了一些关于如何取消订阅 observable 的信息。
谢谢!它有很多优点——在我看来,最大的优点是无需嵌套回调或变异即可“原生”地处理基于时间的值。我没有包含取消订阅,因为 1) 我想让这篇文章保持简洁,2) 在这个例子中,永远不需要取消订阅。也许在另一篇文章中会讲到!
优秀!
解释得非常好。简单明了的例子。
比 200 页的冗长浪费时间的东西好多了。
谢谢。
很棒的文章,谢谢!
很棒的文章,很有趣。我特别喜欢这个语句:
const smoothMove$ = animationFrame$
.withLatestFrom(move$, (frame, move) => move);
真的很优雅!
感谢这篇文章。
一点建议——使用以下代码会更简洁
const smoothMove$ = move$
.withLatestFrom(animationFrame$, (move) => move);
而不是你的解决方案。
在你的情况下,你让处理程序在每个动画帧上调用。
在我的情况下——只在鼠标移动时!
在这种情况下,因为我使用线性插值来动画化元素,即使鼠标停止移动,也必须让处理程序在每个动画帧上调用。