本文译自 An Animated Intro to RxJS
你可能听说过RxJS,ReactiveX,或反应式编程,或者甚至只听过函数式编程。这些术语在谈论最新和最前沿的前端技术时变得越来越突出。如果你像我一样,当你第一次尝试学习它时,你感到很困惑。
根据ReactiveX.io:
ReactiveX是一个库,用于通过使用可观察序列来组成异步和基于事件的程序。
这句话包含了很多要消化的内容。在本文中,我们将采用一种不同的方法来学习RxJS(ReactiveX的JavaScript实现)和Observables
,通过创建反应式动画。
理解Observable
数组是元素的集合,例如[1, 2, 3, 4, 5]。你可以立即得到所有的元素,你可以做的事情像map
,filter
和映射他们。这允许你以任何你想要的方式转换元素的集合。
现在假设阵列中的每个元素随时间发生; 也就是说,你不是立即得到所有的元素,而是一次一个。你可能得到第一个元素在第1秒,下一个在第3秒,依此类推。以下是如何表示:
这可以被描述为值的流,或事件序列,或更确切地,称为observable
。
observable
是随时间的值的集合。
就像使用数组一样,您可以对这些值进行映射,过滤等操作,以创建和组合新的observable
。最后,你可以订阅这些observable
,并在steam
的值之后执行你想要做的事。这是RxJS的源。
RxJS起步
开始使用RxJS的最简单的方法是使用CDN,虽然有很多方法可以安装,这取决于项目的需要。
<script src="https://unpkg.com/@reactivex/rxjs@latest/dist/global/Rx.min.js">script>
一旦你的项目中有RxJS,你可以从任何东西中创建一个observable :
const aboutAnything = 42;
// 从变量创建
// The observable emits that value, then completes.
const meaningOfLife$ = Rx.Observable.just(aboutAnything);
// 从数组或可遍历的结构中创建
// 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'));
// 从事件中创建
// The observable continuously emits events from the event listener.
const mouseMove$ = Rx.Observable
.fromEvent(document.documentElement, 'mousemove');
注意:变量末尾的美元符号
$
是一个约定,表示该变量是一个可观察者。Observable
可以用来对任何可以表示为随时间变化的值的流进行建模,例如事件,Promises,计时器,间隔和动画。
因为,这些可观察的东西不做任何事情,至少直到你实际观察他们。一个简单的订阅将做到这一点,使用创建.subscribe()
:
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使用很多方法来结合Observable
。在这个例子中,可以使用.merge
实现。就像交通多条车道合并成一个单一的车道,这将返回一个包含所有数据的Observable
通过合并多个Observable
.
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 => {
// ...
});
继续,尝试在触摸屏设备上平移左右:
还有很多其他有用的Observable
合并方法,如.switch()
,.combineLatest()
和.withLatestFrom()
,我们继续关注下一个点。
添加平滑的运动
旋转卡动作有点太死板。只要鼠标(或手指)停止,旋转即刻停止。为了解决这个问题,线性内插(线性插值)都可以使用。一般技术中描述这个伟大的教程由雷切尔·史密斯。本质上,而不是从A点跳到B,线性插值会在每一个动画运行一小部分。这将产生一个平滑的过渡,甚至当鼠标/触摸移动已停止。
让我们创建一个函数实现这个功能:计算给定初始值和终值下一个值,采用线性插值:
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有一个叫做Schedulers
的东西,它控制什么时候从一个observable
发出数据,当订阅都开始接收值。
使用Rx.Observable.interval()
,您可以创建一个observable
,它在固定的时间间隔发出值,例如每隔一秒(Rx.Observable.interval(1000))。如果创建一个非常微小的时间间隔,例如Rx.Observable.interval(0)
,并希望它只在每个动画帧上发出值,那么在动画帧内Rx.Scheduler.animationFrame
每隔16到17ms就会发出一个值,如下所示:
const animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame);
结合.withLatestFrom
要创建平滑线性插值,您只需要关心每个动画帧的最新鼠标或触摸位置。要做到这一点,有一个操作符叫.withLatestFrom()
:
const smoothMove$ = animationFrame$
.withLatestFrom(move$, (frame, move) => move);
现在,smoothMove$
是一个新的observable
,move$
只有当animationFrame$
发出一个值时,才会发出最新的值。这是必须的 - 你并不想要的动画帧外发出的值(除非你真的喜欢jank)。第二个参数是描述当组合来自每个可观察的最新值时要做什么的函数。在这种情况下,唯一重要的值是move
值,这是所有返回的值。
与过渡.scan
现在你有一个observable
从move$
每个动画帧发出最新的值,是时候添加线性插值了。.scan()
运算符“积累”从可观察到的当前值和下一个值,提供给需要这些值的函数。
这对于我们的线性插值用例是完美的。记住,我们的lerp(start, end)
函数有两个参数:start(current)
值和end(next)
值。
const smoothMove$ = animationFrame$
.withLatestFrom(move$, (frame, move) => move)
.scan((current, next) => lerp(current, next));
// or simplified: .scan(lerp)
现在,您可以订阅smoothMove$
代替move$
在操作中查看线性插值:
结论
RxJS
不是一个动画库,当然,但对于处理随时间变化的值,它的可组合以及声明性的方式是ReactiveX
演示动画的核心概念。反应式编程是考虑编程的另类方式,具有许多优点:
- 它是声明性的,可组合的和不可变的,避免回调地狱,并使你的代码更简洁,可重用和模块化。
- 它在处理所有类型的异步数据时非常有用,无论是获取数据,通过
WebSockets
进行通信,监听来自多个源的外部事件,甚至是动画。 - “关注的分离” - 您使用
Observable
和运算符声明性地表示您期望的数据,然后在一个单独的上下文环境中处理.subscribe()
,而不会在您的原代码上产生副作用。 - 有这么多语言的实现了它 -
Java
,PHP
,Python
,Ruby
,C#
,Swift
和其他你可能没有听说过的。 - 它不是一个框架,它与许多流行的框架(如React,Angular和Vue)可以非常好地融合在一起。
- 你可以得到行家指点,如果你想要,但
ReactiveX
从被提出到现在实施了近十年前(2009年),由创意所产生的Conal Elliott
和保罗·胡达克 2年前(1997年),在描述功能性反应的动画(惊喜惊喜)。不用说,它经过了足够的测试。
本文探讨了一些有用的部分和RxJS
的概念-创建与订阅.fromEvent()
和.interval()
,对订阅操作.map()
和.scan()
,多个订阅相结合.merge()
和.withLatestFrom()
,并与引入调度Rx.Scheduler.animationFrame
。有很多其他有用的资源学习RxJS:
- ReactiveX:RxJS - 官方文档
- RxMarbles - 用于可视化观察
- Reactive编程的介绍
如果你想进一步深入RxJS动画
(和使用更多的CSS变量),请查看我的幻灯片从CSS Dev Conf 2016
和我2016年的演讲从JSConf冰岛关于反应式动画与CSS变量。这里有一些使用RxJS创建的动画可能对触发灵感有帮助: