RxJS 系列 – 實戰練習

前言

這篇主要是給一些簡單例子, 從中體會 RxJS 在管理上的思路. 

 

Slide Down Effect with Dynamic Content

我在這篇 CSS & JS Effect – FAQ Accordion & Slide Down 講過如何實現 slide down with dynamic content.

效果大概是這樣的

RxJS 的思路步驟

元素 > 事件 > 狀態 > 渲染

1. 先把涉及到的元素找出來. 比如上面的 open button, close button, add more button, card 等等

2. 把監聽的事件找出來, 比如 click, transitionend 等等

3. 抽象出狀態, 然後通過事件改變狀態. 比如 Status: 'opening' | 'opened' | 'closing' | 'closed'

opening 表示正要打開, opened 表示已經打開, closing 表示正要關閉, closed 表示已經關了.

依據上面的操作那麼就是 closed > opening > opened > closing > closed, 當然依據用戶的操作也可以是 closed > opening > closing > closed (在還沒有完全打開的時候 user 就點擊了關閉)

4. 依據狀態對元素進行渲染 (修改 DOM)

RxJS 之管理

從上面幾個步驟可以發現它帶有 mvvm 的思想, 也有 redux 那種 state management 的味道. 

拆分步驟有幾個好處

1. 出現 bug 的時候容易排查定位.

2. 實現的時候可以一步一來.

壞處就是複雜了一些. 所以這其實是一個取捨. 如果你的項目沒有遇到 bug 難定位, 不用分段實現 (爲了休息, 緩口氣) 的話, 其實不用 RxJS 也是 ok 的.

Without RxJS 版本

const openBtn = document.querySelector('.open-btn')!;
const cardWrapper = document.querySelector<HTMLElement>('.card-wrapper')!;
openBtn.addEventListener('click', () => {
  cardWrapper.style.height = `${cardWrapper.scrollHeight}px`;
});

const closeBtn = document.querySelector('.close-btn')!;
closeBtn.addEventListener('click', () => {
  if (cardWrapper.style.height === 'auto') {
    cardWrapper.style.height = `${cardWrapper.scrollHeight}px`;
    requestAnimationFrame(() => {
      cardWrapper.style.removeProperty('height');
    });
  } else {
    cardWrapper.style.removeProperty('height');
  }
});

cardWrapper.addEventListener('transitionend', () => {
  if (cardWrapper.style.height !== '') {
    cardWrapper.style.height = 'auto';
  }
});

const addMoreBtn = document.querySelector('.add-more-btn')!;
const description = document.querySelector('.description')!;
addMoreBtn.addEventListener('click', () => {
  description.textContent = `${description.textContent}\n${description.textContent}`;
});

簡單明瞭, 就是監聽然後操作 DOM, 需要判斷的地方直接讀取 DOM 當前的狀態.

RxJS 版本

先把 element 抓出來

import { fromEvent, map, merge, pairwise, startWith, withLatestFrom } from 'rxjs';

const openBtn = document.querySelector('.open-btn')!;
const closeBtn = document.querySelector('.close-btn')!;
const cardWrapper = document.querySelector<HTMLElement>('.card-wrapper')!;
const description = document.querySelector('.description')!;

監聽 event 並轉換成 state (狀態)

type Status = 'opening' | 'opened' | 'closing' | 'closed';
const opening$
= fromEvent(openBtn, 'click').pipe(map<Event, Status>(() => 'opening')); const closing$ = fromEvent(closeBtn, 'click').pipe(map<Event, Status>(() => 'closing')); const openingOrClosing$ = merge(opening$, closing$);
const transitionend$
= fromEvent(cardWrapper, 'transitionend');
const openedOrClosed$
= transitionend$.pipe( withLatestFrom(openingOrClosing$), map(([_event, openingOrClosing]) => (openingOrClosing === 'opening' ? 'opened' : 'closed')) );
const status$
= merge(openingOrClosing$, openedOrClosed$).pipe( startWith<Status>('closed'), pairwise() );

這裏是考功夫的地方, 需要懂多一點 RxJS 的操作. 很多 stream 都是通過組合搞出來的.

渲染

status$.subscribe(([prevStatus, currStatus]) => {
  switch (currStatus) {
    case 'opening':
      cardWrapper.style.height = `${cardWrapper.scrollHeight}px`;
      break;
    case 'opened':
      cardWrapper.style.height = 'auto';
      break;
    case 'closing':
      {
        if (prevStatus === 'opening') {
          cardWrapper.style.height = '0';
        } else {
          cardWrapper.style.height = `${cardWrapper.scrollHeight}px`;
          requestAnimationFrame(() => {
            cardWrapper.style.height = '0';
          });
        }
      }
      break;
  }
});

這裏和 pure JS 最大的不同是, 它不通過讀取 DOM 發現當前是什麼狀態的, 而是通過 RxJS 把之前的狀態緩存了起來.

這樣代碼就很直觀好理解了.

最後補上

const addMoreBtn = document.querySelector('.add-more-btn')!;
addMoreBtn.addEventListener('click', () => {
  description.textContent = `${description.textContent}\n${description.textContent}`;
});

由於這個很簡單所以不需要用 RxJS 來實現.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章