- 原文地址:An Animated Intro to RxJS
- 原文做者:David Khourshid
- 譯文出自:掘金翻譯計劃
- 譯者: luoyaqifei
- 校對者:vuuihc,AceLeeWinnie
你之前可能聽過 RxJS、ReactiveX、響應式編程,或者只是函數式編程。當咱們談論最新的、最偉大的前端技術時,這些術語正變得愈來愈重要。若是你的學習心路像我同樣,那麼你在最開始學習它時必定也是一頭霧水。javascript
根據 ReactiveX.io:css
ReactiveX 是一個庫,它使用可觀察(observable)序列,用於組織異步的、基於事件的程序。html
單單在這句話裏,就有許多值得咱們琢磨的東西。在本文中,經過建立 響應式動畫,咱們將採用一種不一樣的作法來學習 RxJS(ReactiveX 的 JavaScript 實現)和 Observable(可觀察對象)。前端
數組即元素集合,好比說 [1, 2, 3, 4, 5]
。你可以立刻拿到全部的元素,而且能夠對它們作一些諸如 map 和 filter 這樣的操做。這使得你能夠將元素集合用你想要的方式轉換。java
如今假定數組裏的每一個元素 伴隨時間流動 出現,也就是說,你不是立刻拿到全部的元素,而是一次拿到一個。你可能在第一秒拿到第一個元素,第三秒拿到下一個,諸如此類。就像圖中展示的這樣:react
一個 observable 就是一個伴隨着時間流動的數據集合。git
就像對數組作的那些操做同樣,你能夠對這些數據進行 map、filter 或者作些其餘的操做,來建立和組合新的 observable。最後,你還能夠 subscribe(訂閱)到這些 observable 上,來對最後的數據流進行你想要的任何操做。這些就是 RxJS 的用武之處。es6
開始使用 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複製代碼
讓咱們在實戰中來學習下:
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 / clientHeight
和 pos.x / clientWidth
的值都在 0 到 1 之間,因此乘上 50 再減掉一半(25)會產生 -25 到 25 之間的值,也就是咱們的旋轉值所須要的:
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 =>{// ...});複製代碼
繼續,嘗試着在觸摸設備上左右平移:
也有一些別的 有用的用於組合 observable 的操做符,譬如.switch()
,.combineLatest()
和 .withLatestFrom()
,咱們接下來會討論這些。
由於旋轉卡片實現得太簡潔,其運動有一點點生硬。不管何時鼠標(或手指)一停,旋轉戛然而止。爲了補救這點,可使用線性插值(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% 來靠近下一個(結束)位置。
.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$
上,從而在動做中看到線性插值:
RxJS 不 是一個動畫庫,這是天然,可是使用可組合的、描述式的方式來處理伴隨時間流動的數據,對於 ReactiveX 而言是一個核心概念,所以動畫是一種能很好地展示這個技術的方式。響應式編程是另外一種編程的思惟方式,有許多優勢:
.subscribe()
裏處理反作用,而不是將這些在你的代碼庫裏灑獲得處都是。本文探索了一系列 RxJS 中有用的部分和概念——使用 .fromEvent()
和 .interval()
建立 observable,使用 .map()
和 .scan()
操做 observable,使用 .merge()
和 .withLatestFrom()
結合多個 observable,以及使用 Rx.Scheduler.animationFrame
引入 scheduler。如下是一些學習 RxJS 的其餘有用資源:
若是你想要在 RxJS 的動畫上鑽得更深的話(而且使用 CSS 變量變得更加聲明式),能夠查看 我在 2016 年 CSS 開發大會上的幻燈片 和 我在 2016 年 JSConf Iceland 上的講話。爲了給你更多靈感,這裏有一些使用了 RxJS 來作動畫的代碼: