轻松将rxjs接入react的hooks!observable-hooks源码解析!
前言
我的一个前同事刚入职一家成都本地国企(5点下班太羡慕了),他们用了observable-hooks这个库,我之前也没用过,只知道leetcode前端团队出的一个叫rxjs-hooks的库,自己就尝试去官网看了下,发现确实observable-hooks能力上更胜一筹
但是说实话官网写的文档对于rxjs熟练度一般的人来说真的不好理解,我也算其中,就干脆看源码来理解了。遂成此篇文章,与大家一起学习。每个API都有在线案例,方便大家自己玩。
核心API源码讲解
useObservable
这个API一般用来监听值的变化,然后返回一个Observable,这个就优点像vue、mobx这种响应式了。
一言以蔽之:监听值变化然后产生新的流,就用这个方法,案例如下:
点击button,在线地址:
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]
}
结束
转载自:https://juejin.cn/post/7079704126548312100