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 的同学应该对此有所了解,下次有机会分享一下这个话题。

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

相关推荐
玩电脑的辣条哥3 小时前
Python如何播放本地音乐并在web页面播放
开发语言·前端·python
ew452183 小时前
ElementUI表格表头自定义添加checkbox,点击选中样式不生效
前端·javascript·elementui
suibian52353 小时前
AI时代:前端开发的职业发展路径拓宽
前端·人工智能
Moon.93 小时前
el-table的hasChildren不生效?子级没数据还显示箭头号?树形数据无法展开和收缩
前端·vue.js·html
垚垚 Securify 前沿站3 小时前
深入了解 AppScan 工具的使用:筑牢 Web 应用安全防线
运维·前端·网络·安全·web安全·系统安全
工业甲酰苯胺6 小时前
Vue3 基础概念与环境搭建
前端·javascript·vue.js
mosquito_lover17 小时前
怎么把pyqt界面做的像web一样漂亮
前端·python·pyqt
柴柴的小记9 小时前
前端vue引入特殊字体不生效
前端·javascript·vue.js
柠檬豆腐脑10 小时前
从前端到全栈:新闻管理系统及多个应用端展示
前端·全栈
bin915310 小时前
DeepSeek 助力 Vue 开发:打造丝滑的颜色选择器(Color Picker)
前端·javascript·vue.js·ecmascript·deepseek