[译] 看动画,学 RxJS

看动画,学 RxJS

你之前可能听过 RxJS、ReactiveX、响应式编程,或者只是函数式编程。当咱们谈论最新的、最伟大的前端技术时,这些术语正变得愈来愈重要。若是你的学习心路像我同样,那么你在最开始学习它时必定也是一头雾水。javascript

根据 ReactiveX.iocss

ReactiveX 是一个库,它使用可观察(observable)序列,用于组织异步的、基于事件的程序。html

单单在这句话里,就有许多值得咱们琢磨的东西。在本文中,经过建立 响应式动画,咱们将采用一种不一样的作法来学习 RxJS(ReactiveX 的 JavaScript 实现)和 Observable(可观察对象)。前端

理解 Observable

数组即元素集合,好比说 [1, 2, 3, 4, 5]。你可以立刻拿到全部的元素,而且能够对它们作一些诸如 mapfilter 这样的操做。这使得你能够将元素集合用你想要的方式转换。java

如今假定数组里的每一个元素 伴随时间流动 出现,也就是说,你不是立刻拿到全部的元素,而是一次拿到一个。你可能在第一秒拿到第一个元素,第三秒拿到下一个,诸如此类。就像图中展示的这样:react


这就被称为数据流,或者是事件序列,或者更加贴切地说,一个 observable

一个 observable 就是一个伴随着时间流动的数据集合。git

就像对数组作的那些操做同样,你能够对这些数据进行 map、filter 或者作些其余的操做,来建立和组合新的 observable。最后,你还能够 subscribe(订阅)到这些 observable 上,来对最后的数据流进行你想要的任何操做。这些就是 RxJS 的用武之处。es6

RXJS 上手

开始使用 RxJS 最简单的方式是使用 CDN,尽管根据你的项目需求,有 不少安装它的方法github

HTML

<!-- 最新的,最小化后的 RxJS 版本-->
<scriptsrc="https://unpkg.com/@reactivex/rxjs@latest/dist/global/Rx.min.js"></script>复制代码

一旦你的项目里有了 RxJS,你能够从 任何东西 开始建立一个 observable:编程

JS

const aboutAnything = 42;

// 从 just about anything(单个数据)建立。
// observable 发送这个数据,而后完成。
const meaningOfLife$ = Rx.Observable.just(aboutAnything);

// 从一个数组或一个可迭代对象建立。
// observable 发送数组中的每一个元素,而后完成。
const myNumber$ = Rx.Observable.from([1, 2, 3, 4, 5]);

// 从一个 promise 建立。
// observable 发送最终的结果,而后完成(或者抛出错误)。
const myData$ = Rx.Observable.fromPromise(fetch('http://example.com/users'));

// 从一个事件建立。
// observable 连续地发送事件监听器上的事件。
const mouseMove$ = Rx.Observable
  .fromEvent(document.documentElement, 'mousemove');复制代码

注意:变量后的美圆符($)只是一个约定,用于代表这个变量是 observable。 observable 能够被用于表明任何能够用伴随时间流动的数据流表示的东西,好比事件、Promise、定时执行函数、间隔执行函数和动画。

如今建立的这些 observable 并不作任何有意义的事,除非你真正地 observe 它们。subscription 就是作这个的,能够用 .subscribe() 来建立它。

JS

// 只要咱们从 observable 收到一个数,
// 就将它打印在控制台上。
myNumber$.subscribe(number => console.log(number));

// 结果:
// > 1
// > 2
// > 3
// > 4
// > 5复制代码

让咱们在实战中来学习下:

codepen

JS

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$ observable,每一次 mousemove 事件发生,subscription 将 titleElm.innerHTML 更改成鼠标的当前位置。.map 操做符(与 Array.prototype.map 的工做机制相似)能够帮助简化这段代码:

JS

// 产生如 {x: 42, y: 100} 这种结果,而不是整个事件
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 之间的值,也就是咱们的旋转值所须要的:

codepen

JS

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 融合来的全部数据。

JS

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 =>{// ...});复制代码

继续,尝试着在触摸设备上左右平移:

codepen

也有一些别的 有用的用于组合 observable 的操做符,譬如.switch().combineLatest().withLatestFrom(),咱们接下来会讨论这些。

加入平滑运动(Smooth Motion)

由于旋转卡片实现得太简洁,其运动有一点点生硬。不管何时鼠标(或手指)一停,旋转戛然而止。为了补救这点,可使用线性插值(LERP)。Rachel Smith 的 这个教程 里描述了这种通用技术。从本质上说,再也不直接从 A 点跳到 B 点,LERP 将在每一个动画帧上走一部分路。这就产生了平滑的过渡,即便鼠标/触摸已经中止。

让咱们建立一个函数,这个函数有一个职责:给定一个开始值和一个结束值,使用 LERP 计算下一个值:

JS

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% 来靠近下一个(结束)位置。

Scheduler 和 .interval

如今的问题是,咱们怎么在 RxJS 里表示动画帧?答案是,RxJS 有一个叫作 Scheduler 的东西,它能够控制数据 何时 从一个 observable 被发送,以及一些其余功能,好比何时 subscription 应该开始接收数据。

使用 Rx.Observable.interval(),你能够建立一个在规律定时的间隔上发送数据的 observable,好比每一秒(Rx.Observable.interval(1000))。若是你建立一个微小的间隔,好比 Rx.Observable.interval(0) ,并将它定时为只在使用了 Rx.Scheduler.animationFrame 的每一个动画帧上发送数据的话,一个数据将会每 16 到 17 毫秒被发送,就像你但愿的那样,在一个动画帧内:

JS

const animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame);复制代码

使用 .withLatestFrom 进行结合

为了建立一个平滑的线性插值,你只须要关心在 每一个动画帧 的最新的鼠标/触摸位置。可使用操做符 .withLatestFrom() 来实现:

JS

const smoothMove$ = animationFrame$
  .withLatestFrom(move$, (frame, move) => move);复制代码

如今,smoothMove$ 是一个新的 observable,只有animationFrame$ 发送一个数据时,才会从 move$ 发送最新的数据。这也是咱们想要的——你不想要数据从动画帧外被发送(除非你实在喜欢卡顿)。第二个参数是一个函数,其描述了与每一个 observable 最新的数据结合时须要作什么。在这种状况下,惟一重要的值是 move 值,也就是返回的全部东西。

使用 .scan 进行过渡

既然你有一个 observable ,它能在每一个动画帧上从 move$ 发送最新的数据,是时候加入线性插值了。若是指定一个传入当前和下一个值的函数.scan() 操做符会从一个 observable 中「累积」这些值。

对于咱们的线性插值用例来讲,这是最好不过的了。记住咱们的 lerp(start, end) 函数传入两个参数:start(当前)值和 end(下一个)值。

JS

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

如今,你能够 subscribe 到 smoothMove$ 上,而不是 move$ 上,从而在动做中看到线性插值:

codepen

总结

RxJS 是一个动画库,这是天然,可是使用可组合的、描述式的方式来处理伴随时间流动的数据,对于 ReactiveX 而言是一个核心概念,所以动画是一种能很好地展示这个技术的方式。响应式编程是另外一种编程的思惟方式,有许多优势:

  • 它是声明式的、可组合的,以及不可变的,这避免了回调地狱,让你的代码更加简洁、可复用以及模块化。
  • 它在处理任何类型的异步数据上都颇有用,不管是获取数据、经过 WebSockets 通讯,从多个源头监听外部事件,仍是动画。
  • “关注点分离”——你使用 Observable 和操做符声明式地表示你想要的数据,而后在一个单独的 .subscribe() 里处理反作用,而不是将这些在你的代码库里洒获得处都是。
  • 如此多 语言的实现——Java、PHP、Python、Ruby、C#、Swift,以及别的你甚至没听过的语言。
  • 不是一个框架,不少流行框架(好比 React,Angular 和 Vue)都跟它一块儿工做得很好。
  • 若是你想的话,你能够获得很酷的点,可是 ReactiveX 最先在接近十年之前(2009)被实现,从 Conal Elliott 和 Paul Hudak 十年之前(1997)的想法中被提出,这个想法描述的是函数式响应式动画(真是惊奇啊真是惊奇)。不用说,它是通过战斗考验的。

本文探索了一系列 RxJS 中有用的部分和概念——使用 .fromEvent().interval() 建立 observable,使用 .map().scan() 操做 observable,使用 .merge().withLatestFrom() 结合多个 observable,以及使用 Rx.Scheduler.animationFrame 引入 scheduler。如下是一些学习 RxJS 的其余有用资源:

若是你想要在 RxJS 的动画上钻得更深的话(而且使用 CSS 变量变得更加声明式),能够查看 我在 2016 年 CSS 开发大会上的幻灯片我在 2016 年 JSConf Iceland 上的讲话。为了给你更多灵感,这里有一些使用了 RxJS 来作动画的代码:

相关文章
相关标签/搜索