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

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

相关推荐
cwtlw3 分钟前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥8 分钟前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
米奇妙妙wuu22 分钟前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖27 分钟前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊2 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript