Mobx 思想的实现原理
Mobx 最关键的函数在于 autoRun
,举个例子,它可以达到这样的效果:
const obj = observable({
a: 1,
b: 2
})
autoRun(() => {
console.log(obj.a)
})
obj.b = 3 // 什么都没有发生
obj.a = 2 // observe 函数的回调触发了,控制台输出:2
我们发现这个函数非常智能,用到了什么属性,就会和这个属性挂上钩,从此一旦这个属性发生了改变,就会触发回调,通知你可以拿到新值了。没有用到的属性,无论你怎么修改,它都不会触发回调,这就是神奇的地方。
autoRun 的用途
使用 autoRun
实现 mobx-react
非常简单,核心思想是将组件外面包上 autoRun
,这样代码中用到的所有属性都会像上面 Demo 一样,与当前组件绑定,一旦任何值发生了修改,就直接 forceUpdate
,而且精确命中,效率最高。
依赖收集
autoRun
的专业名词叫做依赖收集,也就是通过自然的使用,来收集依赖,当变量改变时,根据收集的依赖来判断是否需要更新。
实现步骤拆解
为了兼容,Mobx 使用了 Object.defineProperty
拦截 getter
和 setter
,但是无法拦截未定义的变量,为了方便,我们使用 proxy
来讲解,而且可以监听未定义的变量哦。
步骤一 存储结构
众所周知,事件监听是需要预先存储的,autoRun
也一样,为了知道当变量修改后,哪些方法应该被触发,我们需要一个存储结构。
首先,我们需要存储所有的代理对象,让我们无论拿到原始对象,还是代理对象,都能快速的找出是否有对应的代理对象存在,这个功能用在判断代理是否存在,是否合法,以及同一个对象不会生成两个代理。
代码如下:
const proxies = new WeakMap()
function isObservable<T extends object>(obj: T) {
return (proxies.get(obj) === obj)
}
重点来了,第二个要存储的是最重要的部分,也就是所有监听!当任何对象被改变的时候,我们需要知道它每一个 key
对应着哪些监听(这些监听由 autoRun
注册),也就是,最终会存在多个对象,每个对象的每个 key
都可能与多个 autoRun
绑定,这样在更新某个 key
时,直接触发与其绑定的所有 autoRun
即可。
代码如下:
const observers = new WeakMap<object, Map<PropertyKey, Set<Observer>>>()
第三个存储结构就是待观察队列,为了使同一个调用栈多次赋值仅执行一次 autoRun
,所有待执行的都会放在这个队列中,在下一时刻统一执行队列并清空,执行的时候,当前所有 autoRun
都是在同一时刻触发的,所以让相同的 autoRun
不用触发多次即可实现性能优化。
const queuedObservers = new Set()
代码如下:
我们还要再存储两个全局变量,分别是是否在队列执行中,以及当前执行到的 autoRun
。
代码如下:
let queued = false
let currentObserver: Observer = null
步骤二 将对象加工可观察
这一步讲解的是 observable
做了哪些事,首先第一件就是,如果已经存在代理对象了,就直接返回。
代码如下:
function observable<T extends object>(obj: T = {} as T): T {
return proxies.get(obj) || toObservable(obj)
}
我们继续看 toObservable
函数,它做的事情是,实例化代理,并拦截 get
set
等方法。
我们先看拦截 get
的作用:先拿到当前要获取的值 result
,如果这个值在代理中存在,优先返回代理对象,否则返回 result
本身(没有引用关系的基本类型)。
上面的逻辑只是简单返回取值,并没有注册这一步,我们在 currentObserver
存在时才会给对象当前 key
注册 autoRun
,并且如果结果是对象,又不存在已有的代理,就调用自身 toObservable
再递归一遍,所以返回的对象一定是代理。
registerObserver
函数的作用是将 targetObj -> key -> autoRun
这个链路关系存到 observers
对象中,当对象修改的时候,可以直接找到对应 key
的 autoRun
。
那么 currentObserver
是什么时候赋值的呢?首先,并不是访问到 get
就要注册 registerObserver
,必须在 autoRun
里面的才符合要求,所以执行 autoRun
的时候就会将当前回调函数赋值给 currentObserver
,保证了在 autoRun
函数内部所有监听对象的 get
拦截器都能访问到 currentObserver
。以此类推,其他 autoRun
函数回调函数内部变量 get
拦截器中,currentObserver
也是对应的回调函数。
代码如下:
const dynamicObject = new Proxy(obj, {
// ...
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 如果取的值是对象,优先取代理对象
const resultIsObject = typeof result === 'object' && result
const existProxy = resultIsObject && proxies.get(result)
// 将监听添加到这个 key 上
if (currentObserver) {
registerObserver(target, key)
if (resultIsObject) {
return existProxy || toObservable(result)
}
}
return existProxy || result
}),
// ...
})
setter
过程中,如果对象产生了变动,就会触发 queueObservers
函数执行回调函数,这些回调都在 getter
中定义好了,只需要把当前对象,以及修改的 key
传过去,直接触发对应对象,当前 key
所注册的 autoRun
即可。
代码如下:
const dynamicObject = new Proxy(obj, {
// ...
set(target, key, value, receiver) {
// 如果改动了 length 属性,或者新值与旧值不同,触发可观察队列任务
if (key === 'length' || value !== Reflect.get(target, key, receiver)) {
queueObservers<T>(target, key)
}
// 如果新值是对象,优先取原始对象
if (typeof value === 'object' && value) {
value = value.$raw || value
}
return Reflect.set(target, key, value, receiver)
},
// ...
})
没错,主要逻辑已经全部说完了,新对象之所以可以检测到,是因为 proxy
的 get
会触发,这要多谢 proxy
的强大。
可能有人问 Object.defineProperty
为什么不行,原因很简单,因为这个函数只能设置某个 key
的 getter
setter
~。
symbol
proxy
reflect
这三剑客能做的事还有很多很多,这仅仅是实现 Object.observe
而已,还有更强大的功能可以挖掘。
总结
es6 真的非常强大,呼吁大家抛弃 ie11,拥抱美好的未来!