Hi!我是 Jet ,我准备在最近更新系列关于状态管理的源码解读(包含 redux、mobx、jotai),从平时开发中常用的 api 延展,欢迎关注
前文:
Mobx 源码解析(一):从 makeAutoObservable 出发看响应式原理
Mobx 源码解析(二):ComputedValue 初见响应式基石
observer
是 mobx-react-lite
提供的一个高阶组件(Higher Order Component),用于将 React 组件连接到 MobX 状态树,将 React 组件转化为响应式组件。
在 mobx-react
和 mobx-react-lite
中都有 observer
,后者的 observer
不支持将类组件转换成响应式组件,但是拥有更小的体积,具体需要使用哪个就看项目需求,本文对于 observer
的解读基于 mobx-react-lite
,上一波源码
observer
typescript
export function observer<P extends object, TRef = {}>(
baseComponent: React.RefForwardingComponent<TRef, P>,
options?: IObserverOptions
) {
if (isUsingStaticRendering()) {
return baseComponent
}
const realOptions = {
forwardRef: false,
...options
}
const baseComponentName = baseComponent.displayName || baseComponent.name
// 取出组件名称使用 useObserver 进行包裹,后续组件名称会作为 reaction 的 key
const wrappedComponent = (props: P, ref: React.Ref<TRef>) => {
return useObserver(() => baseComponent(props, ref), baseComponentName)
}
wrappedComponent.displayName = baseComponentName
// 做一层 memo 处理
// 那如果有深层次的改变,memo 检测不到怎么办?mobx 的解释是 mobx 本身会去追踪
let memoComponent
if (realOptions.forwardRef) {
memoComponent = memo(forwardRef(wrappedComponent))
} else {
memoComponent = memo(wrappedComponent)
}
// 复制 props
copyStaticProperties(baseComponent, memoComponent)
memoComponent.displayName = baseComponentName
return memoComponent
}
主要是做了 memo
和一些 ref
处理,逻辑在 useObserver
这个 hook 里,继续看 useObserver
useObserver
typescript
// 使用 class 方便 的 debug 和追踪
class ObjectToBeRetainedByReact {}
export function useObserver<T>(fn: () => T, baseComponentName: string = "observed"): T {
if (isUsingStaticRendering()) {
return fn()
}
// 只用来检测组件是否被清理
const [objectRetainedByReact] = React.useState(new ObjectToBeRetainedByReact())
const forceUpdate = useForceUpdate()
// StrictMode/ConcurrentMode/Suspense 可能会渲染/中断多次,防止多次创建
const reactionTrackingRef = React.useRef<IReactionTracking | null>(null)
if (!reactionTrackingRef.current) {
const newReaction = new Reaction(observerComponentNameFor(baseComponentName), () => {
// 回调函数触发,代表可观察对象发生变化,表示我们需要重新渲染组件
// 但是需要判断组件是否到达 useEffect()
if (trackingData.mounted) {
// 已经达到 useEffect() 阶段,组件已经挂载,可以触发更新
forceUpdate()
} else {
// 尚未达到 useEffect() 阶段,所以我们将需要在 useEffect() 到达时(如果到达的话)触发重新渲染
trackingData.changedBeforeMount = true
}
})
// 要考虑组件未挂载、中断情况的情况,进行 mobx 手动废弃之前的 reaction
const trackingData = addReactionToTrack(
reactionTrackingRef,
newReaction,
objectRetainedByReact
)
}
const { reaction } = reactionTrackingRef.current!
React.useDebugValue(reaction, printDebugValue)
React.useEffect(() => {
// 这时可以肯定组件已经挂载,不需要 mobx 去废弃 reaction。在下面的 return 里让 react 组件来废弃
recordReactionAsCommitted(reactionTrackingRef)
if (reactionTrackingRef.current) {
// 已经获取了我们在渲染中设置的 Reaction 对象,只需记录它现在已经挂载,以便允许未来的可观察对象变化触发重新渲染
reactionTrackingRef.current.mounted = true
// 如果在首次挂载前发生了变化,强制更新
if (reactionTrackingRef.current.changedBeforeMount) {
reactionTrackingRef.current.changedBeforeMount = false
forceUpdate()
}
} else {
// 在渲染中设置的 Reaction 对象已被销毁,可能是由于渲染的时机不佳,比如组件暂停了很长时间,导致 Reaction 被清理了,需要重新创建 Reaction 对象
reactionTrackingRef.current = {
reaction: new Reaction(observerComponentNameFor(baseComponentName), () => {
forceUpdate()
}),
mounted: true,
changedBeforeMount: false,
cleanAt: Infinity
}
forceUpdate()
}
// 卸载前废弃 reaction
return () => {
reactionTrackingRef.current!.reaction.dispose()
reactionTrackingRef.current = null
}
}, [])
let rendering!: T
let exception
reaction.track(() => {
try {
rendering = fn()
} catch (e) {
exception = e
}
})
if (exception) {
throw exception
}
return rendering
}
useObserver
内部创建了一个 Reaction
,用来连接组件与 mobx
,让组件订阅到 mobx
使其可以收集到依赖,当数据更新时,能够执行 forceUpdate
来重新渲染组件
我们暂时先不看 Reaction
的原理,先来看看 addReactionToTrack
和 recordReactionAsCommitted
这两个方法
他们俩的作用是保证组件被清理时,需要废弃掉与这个组件关联的 reaction
,但是这样说的话,我们不是只需要在 useEffect
里去执行这个清除就好了吗?
但是在 react
StrictMode/ConcurrentMode/Suspense
模式下,组件可能会执行渲染或者中断多次,无法仅依赖 effect
去做到准确的废弃,所以 mobx
做了这一块功能去确保准确的废弃 reaction
,防止内存的泄漏
我们看一下 addReactionToTrack
和 recordReactionAsCommitted
的定义
addReactionToTrack
typescript
const {
addReactionToTrack,
recordReactionAsCommitted,
resetCleanupScheduleForTests,
forceCleanupTimerToRunNowForTests
} = FinalizationRegistryMaybeUndefined
? createReactionCleanupTrackingUsingFinalizationRegister(FinalizationRegistryMaybeUndefined)
: createTimerBasedReactionCleanupTracking()
这里 mobx
判断了当前环境是否有 FinalizationRegistry
这个 api,关于这个 api 的作用可以看我的这篇文章
ES12 新特性 FinalizationRegistry 的使用
这里我们只看存在 FinalizationRegistry
时,mobx
的源码实现,不存在 FinalizationRegistry
时,简单介绍一下机制
createReactionCleanupTrackingUsingFinalizationRegister
typescript
export function createReactionCleanupTrackingUsingFinalizationRegister(
FinalizationRegistry: NonNullable<typeof FinalizationRegistryMaybeUndefined>
): ReactionCleanupTracking {
// 保存追踪 react 组件
const cleanupTokenToReactionTrackingMap = new Map<number, IReactionTracking>()
let globalCleanupTokensCounter = 1
// 实例化 FinalizationRegistry 对象
const registry = new FinalizationRegistry(function cleanupFunction(token: number) {
// 当 react 组件被销毁时,废弃与之关联的 reaction,并从 map 中清除
const trackedReaction = cleanupTokenToReactionTrackingMap.get(token)
if (trackedReaction) {
trackedReaction.reaction.dispose()
cleanupTokenToReactionTrackingMap.delete(token)
}
})
return {
addReactionToTrack(
reactionTrackingRef: React.MutableRefObject<IReactionTracking | null>,
reaction: Reaction,
objectRetainedByReact: object
) {
const token = globalCleanupTokensCounter++
// 注册清理事件,赋值 reactionTrackingRef.current,保存 react 组件的 state 标记到 map 中
registry.register(objectRetainedByReact, token, reactionTrackingRef)
reactionTrackingRef.current = createTrackingData(reaction)
reactionTrackingRef.current.finalizationRegistryCleanupToken = token
cleanupTokenToReactionTrackingMap.set(token, reactionTrackingRef.current)
return reactionTrackingRef.current
},
// 执行这个函数的时候,react 组件已经挂载了,不需要 mobx 去废弃 reaction 了,组件的 useEffect 会去做这件事
recordReactionAsCommitted(reactionRef: React.MutableRefObject<IReactionTracking | null>) {
// 取消监听清理回调
registry.unregister(reactionRef)
if (reactionRef.current && reactionRef.current.finalizationRegistryCleanupToken) {
cleanupTokenToReactionTrackingMap.delete(
reactionRef.current.finalizationRegistryCleanupToken
)
}
},
forceCleanupTimerToRunNowForTests() {
},
resetCleanupScheduleForTests() {
}
}
}
可以看到,mobx
利用了 FinalizationRegistry
的垃圾回收回调事件实现了废弃 reaction
,还记得 useObserver
的这段代码吗
FinalizationRegistry
去监听 objectRetainedByReact
这个 react state
,当他被回收的时候,说明 react
已经废弃掉这个组件了,那么相对应的 reaction
也需要被废弃
当前环境没有 FinalizationRegistry
时,mobx
是做了一个轮询任务,每隔 10s 去查看一下组件的状态,如果被卸载清除了就去废弃 reaction
总结
那么总结一下 observer
,他做的事情可以概括为以下几点
- 使用 HOC 去包裹并缓存组件,做一些 ref、name 和 props 的处理
- 创建 Reaction 建立组件与 mobx 的绑定,让 mobx 可以收集到依赖并在更新时重新 render
- 处理了组件多次中断/渲染的情况下,确保准确废弃 reaction,防止内存泄漏
那么 Reaction
里又做了什么呢,这个我们下一章继续来深究