Mobx 源码解析(三):看看 observer 高阶组件都做了什么👀

Hi!我是 Jet ,我准备在最近更新系列关于状态管理的源码解读(包含 redux、mobx、jotai),从平时开发中常用的 api 延展,欢迎关注

前文:

Mobx 源码解析(一):从 makeAutoObservable 出发看响应式原理

Mobx 源码解析(二):ComputedValue 初见响应式基石

observermobx-react-lite 提供的一个高阶组件(Higher Order Component),用于将 React 组件连接到 MobX 状态树,将 React 组件转化为响应式组件。

mobx-reactmobx-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 的原理,先来看看 addReactionToTrackrecordReactionAsCommitted 这两个方法

他们俩的作用是保证组件被清理时,需要废弃掉与这个组件关联的 reaction,但是这样说的话,我们不是只需要在 useEffect 里去执行这个清除就好了吗?

但是在 react StrictMode/ConcurrentMode/Suspense 模式下,组件可能会执行渲染或者中断多次,无法仅依赖 effect 去做到准确的废弃,所以 mobx 做了这一块功能去确保准确的废弃 reaction,防止内存的泄漏

我们看一下 addReactionToTrackrecordReactionAsCommitted 的定义

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 里又做了什么呢,这个我们下一章继续来深究

相关推荐
昨天;明天。今天。1 小时前
案例-表白墙简单实现
前端·javascript·css
数云界1 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd1 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常1 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer1 小时前
Vite:为什么选 Vite
前端
小御姐@stella1 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing1 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd1 小时前
前端知识汇总(持续更新)
前端
万叶学编程4 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js