最近在实现一个树形组件时,发现了之前实现的 tooltip 功能有些性能问题,原因在组件中滚动或滑动鼠标时,会频繁触发 tooltip 的显示与隐藏,故决定利用 RxJS 替换原有的原生方案。

原有方案

原有方案是直接添加 mouseentermouseleave 事件实现:

function setTooltip(elem) {
  function onMouseEnter(event) {
    // 显示 tooltip
  }

  function onMouseLeave(event) {
    // 隐藏 tooltip
  }

  elem.addEventListener("mouseenter", onMouseEnter);
  elem.addEventListener("mouseleave", onMouseEnter);
}

现有方案

为避免不必要的 tooltip 显示逻辑,可以采用延迟触发 onMouseEnter 的方法:当用户在时间阈值内就离开了元素(即 mouseleave 触发)即表示用户并不关心这个 tooltip。第一感觉是可以利用 switchMapdelay 来做到:

import { fromEvent, merge, iif, of } from "rxjs";
import { map, switchMap, delay } from "rxjs/operators";

function setTooltip(elem, delayDuration = 100) {
  function onMouseEnter(event) {
    // 显示 tooltip
  }

  function onMouseLeave(event) {
    // 隐藏 tooltip
  }

  const showStream$ = fromEvent(elem, "mouseenter").pipe(
    map((event) => ({
      event,
      show: true,
    }))
  );
  const hideStream$ = fromEvent(elem, "mouseleave").pipe(
    map((event) => ({
      event,
      show: false,
    }))
  );
  merge(showStream$, hideStream$)
    .pipe(
      // prettier-ignore
      switchMap(data =>
          iif(
              () => data.show,
              of(data).pipe(delay(delayDuration)), // 延迟触发显示
              of(data) // A
          )
      )
    )
    .subscribe(({ show, event }) => {
      if (show) {
        onMouseEnter(event);
      } else {
        onMouseLeave(event);
      }
    });
}

当然此种方式,可能需要在 onMouseLeave 中判断下是否已经显示了 tooltip,再将其移除/隐藏。当然也可以在上面的 A 行处增加 filter,更加符合 RxJS 的 style:

// checkHasTooltip: () => boolean

iif(
  () => data.show,
  of(data).pipe(delay(delayDuration)),
  of(data).pipe(filter(() => checkHasTooltip()))
)

iif 操作符和三目操作符比较像,第一个参数是一个 () => boolean 类型的函数,如果为真返回第二个参数的 Observable,否则返回第三个参数的 Observable。

总结

虽然看起来代码变得很繁琐,而且利用原生的方案通过 setTimeout 等手段也能实现类似的功能,但在理解的基础上使用 RxJS 会使逻辑上更加顺畅。