轻松将rxjs接入react的hooks!observable-hooks源码解析!

2,599 阅读2分钟

前言

我的一个前同事刚入职一家成都本地国企(5点下班太羡慕了),他们用了observable-hooks这个库,我之前也没用过,只知道leetcode前端团队出的一个叫rxjs-hooks的库,自己就尝试去官网看了下,发现确实observable-hooks能力上更胜一筹

但是说实话官网写的文档对于rxjs熟练度一般的人来说真的不好理解,我也算其中,就干脆看源码来理解了。遂成此篇文章,与大家一起学习。每个API都有在线案例,方便大家自己玩。

核心API源码讲解

useObservable

这个API一般用来监听值的变化,然后返回一个Observable,这个就优点像vue、mobx这种响应式了。

一言以蔽之:监听值变化然后产生新的流,就用这个方法,案例如下:

点击button,在线地址:

codesandbox.io/s/sharp-haw…

import "./styles.css";
import { useObservable } from "observable-hooks";
import { map } from "rxjs";
import { useState } from "react";

const App = (props) => {
  const [showPanel] = useState("hello");

  // 监听 props 或 state 变化
  const enhanced$ = useObservable(
    (inputs$) => inputs$.pipe(map(([showPanel]) => showPanel + "world")),
    [showPanel]
  );
  return (
    <div className="App">
      <h1>{showPanel}</h1>
      <button
        onClick={() => {
          // 这个方法
          enhanced$.subscribe((value) => alert(value));
        }}
      >
        click
      </button>
    </div>
  );
};

export default App;

源码分析,关键代码:

export function useObservable(
  init,
  inputs?: [...TInputs]
): Observable<TOutput> {

  const inputs$Ref = useRefFn(() => new BehaviorSubject(inputs))
  const source$Ref = useRefFn(() => init(inputs$Ref.current))

  const firstEffectRef = useRef(true)
  useEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$Ref.current.next(inputs)
  }, inputs)

  return source$Ref.current
}

useRefFn是借助useRef来让new BehaviorSubject(inputs),不用每次都new 实例化,我们可以重复用第一次产生的new BehaviorSubject,源码很简单,请注意init() 只会调用一次,如下:

/**
 * 一个返回值的函数。 只会被调用一次
 */
 export function useRefFn<T>(init: () => T) {
  const firstRef = useRef(true)
  // 请注意init() 只会调用一次
  const ref = useRef<T | null>(null)
  if (firstRef.current) {
    firstRef.current = false
    ref.current = init()
  }
  return ref;
}

好了我们接着看useObservable源码,首先创建了一个new BehaviorSubject(inputs),inputs就是useObservable的第二个参数,依赖数据,这些数据变化useObservable就会重新推送流。

重新推送的代码是:

  useEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$Ref.current.next(inputs)
  }, inputs)

可以看到是借助useEffect来监听inputs的变化,然后inputs$Ref.current.next(inputs),来重新推送流。

最后,我们看一下这一句

  const source$Ref = useRefFn(() => init(inputs$Ref.current))

就是把new BehaviorSubject(inputs)流传入给init函数,init函数是useObservable的第一个参数,是我们自定义的。

useLayoutObservable

与 useObservable 基本一样,不同的是底下使用 useLayoutEffect 监听改变。

如果需要在下次浏览器绘制前拿到值可以用它, 所以源码跟我们之前是一样的,就是把useEffect改成了useLayoutEffect而已。

useObservableCallback

一言以蔽之,这个useObservableCallback一般用来给事件监听的,事件一变化就产生新的流。需要注意的是,需要自己手动去订阅。

案例如下(当input值变化时,注意看控制台信息变化): codesandbox.io/s/affection…

import "./styles.css";
import { pluck, map } from "rxjs";
import { useObservableCallback } from "observable-hooks";
import { useEffect } from "react";

const App = (props) => {
  const [onChange, outputs$] = useObservableCallback((event$) =>
    event$.pipe(pluck("currentTarget", "value"))
  );
  useEffect(() => outputs$.subscribe((v) => console.log(v)));
  return <input type="text" onChange={onChange} />;
};

export default App;

源码如下:

export function useObservableCallback(
  init,
  selector
) {
  const events$Ref = useRefFn(new Subject())
  const outputs$Ref = useRefFn(() => init(events$Ref.current))
  const callbackRef = useRef((...args) => {
    events$Ref.current.next(selector ? selector(args) : args[0])
  })
  return [callbackRef.current, outputs$Ref.current]
}
  • 首先events$Ref就是一个new Subject
  • 然后定义一个消费流outputs$Ref,我们传入的自定义init函数第一个参数就是上一步的new Subject
  • callbackRef是一个注册函数,常用于给事件,也就是事件触发,就给outputs$Ref推送数据

当然需要回调函数传入多个参数才需要selector,大家现在只是入门,等用到的时候再了解不迟。

useSubscription

useSubscription说白了,就是subscribe的hooks而已。

源码如下:

源码只做了一个特殊处理需要注意,其他的不用看,就是subscrible

if (input$ !== argsRef.current[0]) {
     // stale observable
     return
}
export function useSubscriptionInternal(
  args
) {
  const argsRef = useRef(args)
  const subscriptionRef = useRef()

  useEffect(() => {
    argsRef.current = args
  })

  useEffect(() => {
    const input$ = argsRef.current[0]

    const subscription = input$.subscribe({
      next: value => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const nextObserver =
          argsRef.current[1].next ||
          argsRef.current[1]
        if (nextObserver) {
          return nextObserver(value)
        }
      },
      error: error => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const errorObserver =
          argsRef.current[1].error ||
          argsRef.current[2]
        if (errorObserver) {
          return errorObserver(error)
        }
        console.error(error)
      },
      complete: () => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const completeObserver =
          argsRef.current[1].complete ||
          argsRef.current[3]
        if (completeObserver) {
          return completeObserver()
        }
      }
    })

    subscriptionRef.current = subscription

    return () => {
      subscription.unsubscribe()
    }
  }, [args[0]])

  return subscriptionRef
}

useLayoutSubscription

与 useSubscription 一样,除了 subscription 是通过 useLayoutEffect 触发。

当需要在 DOM 绘制前拿到值时会有用。

尽量少用,因为其是在浏览器绘制前同步调用。过多的同步值产生会延长组件的 commit 周期。

useObservableState

这个函数可以当做useState或者useReducer,我建议自己的项目都是用useReducer,不要用state,因为一般你的状态都是分类的,比如请求一个数据,这个数据是一个变量,但同样伴随的是,请求数据时的loading状态,loading和请求的本次数据是一体的,为啥要用两个useState呢,看起来真别扭。

案例加一减一如下:

在线代码: codesandbox.io/s/kind-jasp…

import "./styles.css";
import { scan } from "rxjs";
import { useObservableState } from "observable-hooks";

const App = (props) => {
  const [state, dispatch] = useObservableState(
    (action$, initialState) =>
      action$.pipe(
        scan((state, action) => {
          switch (action.type) {
            case "INCREMENT":
              return {
                ...state,
                count:
                  state.count + (isNaN(action.payload) ? 1 : action.payload)
              };
            case "DECREMENT":
              return {
                ...state,
                count:
                  state.count - (isNaN(action.payload) ? 1 : action.payload)
              };
            default:
              return state;
          }
        }, initialState)
      ),
    () => ({ count: 0 })
  );

  return (
    <div className="App">
      <h1>{state.count}</h1>
      <button
        onClick={() => {
          dispatch({ type: "INCREMENT" });
        }}
      >
        加一
      </button>
      <button
        onClick={() => {
          dispatch({ type: "DECREMENT" });
        }}
      >
        减一
      </button>
    </div>
  );
};

export default App;

所以我们这里只介绍怎么使用useObservableState实现useReducer,以下是关键代码

  • state是我们要的数据
  • callback会把你要传入的值传入到第一个参数state$OrInit中,也就是我们的自定义流中
  • useSubscription最终会把流中处理后的数据setState返回出最新的state
useObservableStateInternal(
  state$OrInit,
  initialState){
    const init = state$OrInit
    const [state, setState] = useState(initialState)

    const input$Ref = useRefFn(new Subject())

    const state$ = useRefFn(() => init(input$Ref.current, state)).current
    const callback = useRef((state) =>
      input$Ref.current.next(state)
    ).current

    useSubscription(state$, setState)

    return [state, callback]

}

结束