findxc / blog

88 stars 5 forks source link

一个 RxJS 实际应用的例子 #86

Open findxc opened 1 year ago

findxc commented 1 year ago

需求是啥

最近做了一个需求,一个支持多列排序的表格,希望在按住 Shift 键后,点击不同列设置好排序了,松开 Shift 键后才去请求数据。

image

来想一下,如果是你,你会怎么去实现这个功能?

因为项目技术栈是 Angular ,自带 RxJS ,就正好学习了一下 RxJS ,然后用了一下,发现还不错。在做完这个功能后我才真的理解了 RxJS 文档中的 Think of RxJS as Lodash for events. ,也才真的感受到了它的 Purity 的美妙,官方文档有个点击计数的例子,如下:

// without RxJS
let count = 0;
document.addEventListener('click', () => console.log(`Clicked ${++count} times`));

// with RxJS
import { fromEvent, scan } from 'rxjs';
fromEvent(document, 'click')
  .pipe(scan((count) => count + 1, 0))
  .subscribe((count) => console.log(`Clicked ${count} times`));

使用 RxJS 的话,我们不需要额外定义 count 变量,我们只用写好我们想要的逻辑,然后在 subscribe 中就能拿到想要的值,与 count 相关的代码逻辑十分集中,这种逻辑的集中在更复杂的场景中会显得更加诱人。(相信你遇到过业务迭代时,你想修改一个已有功能,然后发现这个功能涉及到多个变量并且多个地方在修改这些变量...啊...熊熊咆哮...)

来看代码

RxJS 版本

每次排序有变化时, sortAction$ 都会产生一个新值,然后我们并不是希望每次有新值就去请求数据,而是要根据 Shift 键状态来做不同处理,所以我们封装一个函数输入参数是 sortAction$ ,返回值是处理好之后的 Observable ,然后只用 subscribe 它然后发请求就好了,这个处理好的 Observable 只会在有必要的时候产生新值~完整代码见 multiple-column-sorting-by-rxjs

import {
  Observable,
  distinctUntilChanged,
  filter,
  first,
  fromEvent,
  map,
  merge,
  of,
  switchMap,
  withLatestFrom,
} from 'rxjs'

export const combineSortActionObservableWithShiftKeyPress = <T>(
  sortAction$: Observable<T>
): Observable<T> => {
  const shiftKeyPressd$ = merge(
    fromEvent<MouseEvent>(document, 'click', { capture: true }),
    fromEvent<KeyboardEvent>(document, 'keyup').pipe(
      filter(event => event.key === 'Shift')
    )
    // fromEvent(document, 'mouseleave').pipe(map(() => ({ shiftKey: false }))),
    // fromEvent(document, 'visibilitychange').pipe(
    //   filter(() => document.hidden),
    //   map(() => ({ shiftKey: false }))
    // )
  ).pipe(
    map(event => event.shiftKey),
    distinctUntilChanged()
  )

  return sortAction$.pipe(
    withLatestFrom(shiftKeyPressd$),
    switchMap(([params, pressed]) => {
      if (!pressed) {
        return of(params)
      }
      return shiftKeyPressd$.pipe(
        first(pressed => !pressed),
        map(() => params)
      )
    })
  )
}

React Hook 版本

useMultiSort 接受一个参数 getList ,返回 onMultiSort ,每次排序有变化时, onMultiSort 都需要被调用,然后在内部判断只在必要时去调用 getList 。完整代码见 multiple-column-sorting-by-react-hook

import { useCallback, useEffect, useRef, useState } from 'react'
import { SortModelItem } from './FakeTable'

const useMultiSort = (
  getList: (value: SortModelItem[]) => void
): ((sortModel: SortModelItem[]) => void) => {
  const [shiftKeyPressd, setShiftKeyPressd] = useState(false)
  const sortActionRef = useRef<SortModelItem[]>()

  useEffect(() => {
    const click = (event: MouseEvent) => {
      setShiftKeyPressd(event.shiftKey)
    }
    const keyup = (event: KeyboardEvent) => {
      if (event.key === 'Shift') {
        setShiftKeyPressd(false)
        if (sortActionRef.current) {
          getList(sortActionRef.current)
          sortActionRef.current = undefined
        }
      }
    }
    document.addEventListener('click', click, { capture: true })
    document.addEventListener('keyup', keyup)
    return () => {
      document.removeEventListener('click', click)
      document.removeEventListener('keyup', keyup)
    }
  }, [getList])

  const onMultiSort = useCallback(
    (sortModel: SortModelItem[]) => {
      if (shiftKeyPressd) {
        sortActionRef.current = sortModel
      } else {
        getList(sortModel)
      }
    },
    [shiftKeyPressd, getList]
  )

  return onMultiSort
}

export default useMultiSort

来个总结

两个对比一下, RxJS 版本的代码逻辑是不是集中和清晰很多?它不需要额外定义变量,然后再在事件中去更新变量值。呃,当然,如果你对 RxJS 的 API 一脸懵的话...恩...咱多看两眼?

哈哈哈,恩,我并不是想安利 RxJS 啦,说老实话,除了这次这个需求,我之前没感受到 RxJS 有多好用,它真的太挑使用场景了,大多数时候我内心都在想, Promise 不香吗, async / await 不香吗哈哈哈。但是针对我这里的这个场景,我不得不说, RxJS 还是挺不错的,它让我可以把相关代码都写到一堆这种感觉就很爽...哈哈哈...

findxc commented 10 months ago

补充一下,上面 RxJS 的实现,其实可以封装成一个自定义的 operator ,下面是一个自定义的 operator 的例子。

// an example of how to write a custom operator
const debug = description => source =>
  source.pipe(tap(v => console.log(description, v)))

// how to use:
xxx$.pipe(
  debug('varA: ')
)

在使用 RxJS 的过程中,如果某些逻辑经常用到,那么可以尝试把这些逻辑封装为自定义 operator 来减少业务代码中的 copy & paste 。