阅读 119

Mobx 源码分析 - computed

computed 只会在值用到的时候才会执行函数

用法

下文会以此作为依据进行讲解

class Foo{
    @computed get age() { return expr; } // 推荐用法
}
复制代码

源码剖析

属性劫持

通过调用 createPropDecorator 方法给原型对象创建一个不可枚举的 __mobxDecorators 属性对象,age 会作为属性对象的 key 存在,此方法会返回一个描述符。

// 添加 __mobxDecorators 属性
const inheritedDecorators = target.__mobxDecorators
addHiddenProp(target, "__mobxDecorators", { ...inheritedDecorators })
// 描述符
{
    configurable: true,
    enumerable: enumerable,
    get() {
        initializeInstance(this)
        return this[prop]
    },
    set(value) {
        initializeInstance(this)
        this[prop] = value
    }
}
复制代码

我们发现这里劫持了属性的 getset 操作,每当执行 getset 操作之前,都会先执行 initializeInstance

initializeInstance

判断实例上 __mobxDidRunLazyInitializers 是否为 true,如果是则代表对象已经处于可监听状态。否则取原型上的 __mobxDecorators 对象,并遍历此对象的每一个 propertyCreator 方法,也就是之前 createPropDecorator 方法的第二个参数。propertyCreator 方法内部把描述符上 getsetdecoratorArgs[0] 当做 defineComputedProperty 第三个参数传递进去。

const { get, set } = descriptor
const options = decoratorArgs[0] || {}
defineComputedProperty(instance, propertyName, { get, set, ...options })
复制代码

defineComputedProperty

const adm = asObservableObject(target)
options.name = `${adm.name}.${propName}`
options.context = target
adm.values[propName] = new ComputedValue(options)
Object.defineProperty(target, propName, generateComputedPropConfig(propName))
复制代码

asObservableObject 主要作用为在实例上新增不可枚举的 $mobx 属性,age 会作为属性的 key 存在,属性值为 ObservableObjectAdministration 实例。

修改 adm.values[propName]ComputedValue 的实例,实例上新增不可枚举的 age 属性,并劫持 getset,每当访问或设置 age 属性时,实际上是访问 $mobx 原型上的 readwrite

function generateComputedPropConfig(propName){
    return {
        get() {
                return getAdministrationForComputedPropOwner(this).read(this, propName)
            },
        set(v) {
            getAdministrationForComputedPropOwner(this).write(this, propName, v)
        }
    }
}

function getAdministrationForComputedPropOwner(owner) {
    const adm = owner.$mobx
    if (!adm) {
        initializeInstance(owner)
        return owner.$mobx
    }
    return adm
}
复制代码

ObservableObjectAdministration

前文说过,访问 age 实际上是访问 $mobx 上的 read,而 read 又返回 values.age 值,也就是相当于访问 ComputedValue 实例的 get 方法。总结下,访问 age 相当于访问 ComputedValue 实例的 get 方法。

ComputedValue

如果当前不处于事务处理,又没有观察者且 keepAlivefalse 状态,则开启事务处理,并执行 age 函数,并把返回值保存在 value 中,并结束事务处理。

if (globalState.inBatch === 0 && this.observers.length === 0 && !this.keepAlive) {
    // NOT_TRACKING
    if (shouldCompute(this)) {
        this.warnAboutUntrackedRead();
        startBatch(); // See perf test 'computed memoization'
        this.value = this.computeValue(false);
        endBatch();
    }
}
复制代码

如果当前处于事务处理或有观察者或 keepAlivetrue,则调用 reportObserved,在正在跟踪的 derivation 中加入当前实例,并设置实例已处于监听状态;如果当前实例没有观察者且当前处于事务中,则把当前实例放入到队列中,等待移除监听状态。执行 age 函数,并刷新监听对象依赖关系,把 age 函数执行结果返回出去,并用此值与旧值进行比对,如果不一致,则使用新值,并改变内部状态。

function computeValue(track){
    ...
    res = trackDerivedFunction(this, this.derivation, this.scope)
    ...
}
复制代码