[譯] 看動畫,學 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 來作動畫的代碼:

相關文章
相關標籤/搜索