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 来实现.

 

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