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

相关推荐
辻戋2 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保2 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun3 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp3 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.4 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl6 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js