mobx 是如何运行的?

大家好,我是火焱。

新团队很多项目用到了 mobx,刚上手 mobx 时遇到过几次「数据变了为什么组件没渲染」的问题,于是借机学习了一下 mobx 的源码。

示例

mobx 使用起来还是比较简洁的,首先定义数据,然后消费数据:

javascript 复制代码
// 定义 mobx 数据
class TimerStore {
    secondsPassed = 0
  
    constructor() {
        makeAutoObservable(this)
    }
  
    increase() {
        this.secondsPassed += 1
    }
  
    reset() {
        this.secondsPassed = 0
    }
}

// 定义组件
function TimerView({ timer }) {
   const store = new TimerStore();
   return (
     <button onClick={() => store.reset()}>
     	Seconds passed: {store.secondsPassed}
   	 </button>
   );
}
export observer(TimerView);

结构推导

根据上面的例子,我们来做一个简单的推导,初始结构应该是这样的:

数据 ------> 组件

如果我们需要感知到数据的读写,就需要对数据做一层加工:

数据 ------> 可观察数据 ------> 组件

可观察数据与组件直接绑定了,如果想要框架无关,再加一层:

数据 ------> 可观察数据 ------> Reaction ------> 组件(副作用)

中间两层结合 mobx 的实现,类图如下:

其中:观察者 是 Reaction(还有 Derivation),被观察者是 ObservableValue。

数据处理

数据 ----> 可观察数据

通过 makeObservable 可以将数据转化成可观察数据,mobx 将不同类型数据的逻辑封装到了不同的 annotation 里,下面代码的执行链路为 adm.make_ ---> annotation.make_:

javascript 复制代码
export function makeObservable(target, annotations) {
    const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
    
    // 执行 annotations
    ownKeys(annotations).forEach(key => adm.make_(key, annotations![key]))
    
    return target
}

对于普通数据, annotation.make_ 中调用了 defineProperty,通过 defineProperty 我们可以感知到数据的读取与写入:

javascript 复制代码
const obj = { a: 'aa', b: 'bb' }
Object.defineProperty(obj, 'a', {
  // getter
  get() {
    // 主要分两步
    // 1. reportObserved(this)
    // 2. 返回 a 的值
  },
  // setter
  set() {
    // 主要分两步:
    // 1. reportChanged
    // 2. 设置 a 的值
  }
});

reportObserved 用来上报自己被观察了,其中 globalState.trackingDerivation 用来保存当前正在收集依赖的观察者,收集依赖的意思是观察者需要保存它访问了哪些数据:

arduino 复制代码
export function reportObserved(observable: IObservable): boolean {
    // 正在收集依赖的观察者
    const derivation = globalState.trackingDerivation
    derivation.newObserving_![derivation.unboundDepsCount_++] = observable       
    return false
}

reportChanged 用来通知观察者数据有变化:

javascript 复制代码
// Atom
public reportChanged() {
  propagateChanged(this)
}

//
export function propagateChanged(observable) {
  observable.observers_.forEach(d => {
    // 对于 d 为 Reaction 的情况
    // 调用链为:onBecomeState_ -> Reaction.onInvalidate_ -> forceUpdate 组件
    d.onBecomeStale_()
  })
}

到此,数据的读写都已经处理完成,下面看一下如何与组件联动。

组件处理

组件 ----> 观察者组件

将组件转化成观察者组件,同样分两步,1、依赖收集;2、组件渲染;

javascript 复制代码
// 让 react 组件成为可观察组件
export function observer(baseComponent) {
    let render = baseComponent
    let observerComponent = (props, ref) => {
        return useObserver(() => render(props, ref), baseComponentName)
    }
    return observerComponent
}

//
export function useObserver(fn, baseComponentName) {
    // setState 可以触发组件渲染
    // 可以定义一个渲染组件的函数
    const [, setState] = React.useState()
    const forceUpdate = () => setState([] as any)

    const admRef = React.useRef<ObserverAdministration | null>(null)
    const adm = admRef.current!

    React.useEffect(() => {
      // 观察的数据如果有变化,触发 forceUpdate
      // 第二个参数是 onInvalidate_,会被 onBecomeState_ 调用
      adm.reaction = new Reaction(`observer${baseComponentName}`, () => {
          forceUpdate()
      })
    }, [])

    // 保存渲染后的组件
    let rendering;
    // 收集依赖
    adm.reaction.track(() => {
        rendering = fn()
    })

    return rendering
}

track 主要是调用了 trackDerivedFunction:

javascript 复制代码
// Reaction
track(fn: () => void) {
  // 建立可观察数据与该 Reaction 的关联
  const result = trackDerivedFunction(this, fn, undefined)
}

trackDerivedFunction 用来触发依赖收集,也就是把被观察者保存到观察者中,其中保存的动作是在「数据处理」的 getter 中:

typescript 复制代码
export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
  const prevTracking = globalState.trackingDerivation
  // 保存正在收集依赖的观察者
  globalState.trackingDerivation = derivation
  // 组件渲染
  const result = f.call(context)
  // 恢复堆栈
  globalState.trackingDerivation = prevTracking

  return result
}

总结

mobx 的核心原理与 vue 的双向绑定类似,都主要涉及数据访问拦截、依赖收集等,思路相差不大。

依赖收集是个不错的思路,然而 React 天然不支持,用到过 Context 的同学应该对此有所了解,下次有机会分享一下这个话题。

如果本文对你有启发,欢迎点赞、评论。

相关推荐
世俗ˊ19 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92119 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_24 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人32 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript