大家好,我是火焱。
新团队很多项目用到了 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 的同学应该对此有所了解,下次有机会分享一下这个话题。
如果本文对你有启发,欢迎点赞、评论。