前言

前一篇文章中,通过watch属性的工作来简要说明Watcher类的最最基本结构,以及最简单工作流程,除此之外,Vue.prototype.$watch的工作原理也是类似的。同时也看到了状态变动时,watch指定的函数被执行,照这个思路延伸下去,computed属性也可以用这种方式实现。先执行一遍computed指定的函数(实际上就是一个运算过程),在运算的过程中会访问到所依赖的数据,这些依赖的数据发生变化的时候,在执行一下这个运算过程得到新值,大体看来就是computed属性的作用了。所以本文的目标用例:

const v = new Vue({
    data() {
        return {
            a: 1,
            b: 2
        }
    },
    computed: {
        sum() {
            return this.a + this.b
        }
    }
})

expect(v.sum).toBe(3)
v.b = 3
expect(v.sum).toBe(4)

分析

computed选项提供计算属性的相关算法,计算属性是基于实例基本状态以及其他计算属性的扩展,或者说是多个可计算属性的聚合。这个聚合通过函数的方式呈现,也可以理解成一个计算过程,计算过程产生的结果,就是计算属性。因此,为了得到计算属性的结果,必须要在需要计算的时机执行响应的计算过程,而这个时机也通过订阅/通知体系来完成。

回顾一下watch选项的动机,想要在某个属性发生变化时执行一些操作,观察的目标是单一的。如果扩展一下:

const v = new Vue({
    data() {
        return {
            a: 1,
            b: 1,
            sum: 2
        }
    },
    watch: {
        a(newValue) {
            this.sum = this.b + newValue
        },
        b(newValue) {
            this.sum = this.a + newValue
        }
    }
})

期望的是sum作为a和b求和的结果,a和b变化的时候可以重新计算。假设watch可以处理这样的情形:

watch: {
    'a+b'(newValue) {
        this.sum = newValue
    }
}

也是可以解决,但是对于复杂计算,Watcher还要具备解析表达式的功能,对于计算属性这个需求来说就太重了。那么关注点回到sum上,访问this.sum的时候通过getter,除了收集依赖的逻辑,就是从数据源拿数据返回出去,如果在getter中知道sum的结果总是通过a+b来确定的,那么就不需要那么多观察,只需要在访问sum的时候去求一下值即可。除此之外,计算属性通常是为了解决多个状态聚合使用时的代码复用性,依赖的属性是响应式的,计算属性也应当具备响应式能力。但是计算属性的结果由计算过程决定,不像普通的属性可以直接复制操作,因此通知并不能在setter中完成(甚至应当屏蔽计算属性的setter过程),考虑到与被聚合状态的一致性,被聚合状态发生改变时会触发计算过程的重新执行,因此计算过程的执行可以作为通知的重要时机。

编写

处理computed选项

在之前代码的基础上,要为Vue增加处理computed选项的能力。计算属性是可以通过实例直接访问到,所以第一步应该是这样的:

/**
 * 处理options中的computed部分
 */
initComputed() {
    let computed = this.$options.computed

    for(let key in computed) {
        const fn = computed[key]

        Object.defineProperty(this, key, {
            get() {
                return fn()
            }
        })
    }
}

这样只实现了一半,只有在主动访问计算属性时,才能计算最新值,这个时候就需要扩展一下Watcher的思路。之前实现的Watcher只能处理单一属性,可以理解为Watcher实例与目标属性建立一对一关系;计算过程包含对多个属性的访问,按照访问就是提交订阅的思路,计算过程会与接触到的属性建立一对多的关系。与单一属性建立关系的Watcher被通知,执行用户绑定的回调;与多个属性建立关系的Watcher被通知,重新执行一下计算过程,然后通知那些订阅了计算属性的Watcher。

扩展Watcher

Watcher可以观察单一属性,现在要让它能够观察一组属性,这一组属性之所以成组,是因为它们在同一个计算过程中被使用。按照这个思路,构造Watcher时,观察目标就需要进行扩展:

!!!注:观察单一属性时,可以理解为只访问该属性的计算过程。因此expOrFn虽然传入的形式不同,但是可以转化成相同的逻辑。

constructor(vm: Vue, expOrFn: string | Function, cb: Function)

被观察的一组属性发生变化时,Watcher实例被通知,首先要重新执行计算过程,那这个执行过程的结果需要进行保存。构造的时候求值是为了提交一组订阅,更新的时候先求值再对外通知,求值的过程可以提出来:

constructor(vm: Vue, expOrFn: string | Function, cb: Function) {
    this.vm = vm
    this.expOrFn = expOrFn
    this.cb = cb

    // 将要观察的目标,不管是单一属性,还是计算过程(相当于一组属性)
    // 统一化保存起来
    if (typeof expOrFn === 'string') {
        // 将观察单一属性的要求,也转换成一个计算过程
        this.getter = function () {
            return vm[expOrFn]
        }
    }

    if (typeof expOrFn === 'function') {
        this.getter = expOrFn
    }

    // 兜底
    if (!this.getter) {
        this.getter = noop
    }

    // 已知对vm.expOrFn进行求值相当于一个订阅
    // 先把自身记录在一个全局的范围内,标记自己是正在求值的观察者
    pushTarget(this)
    this.value = this.get()
    popTarget()
}

/**
 * 求观察目标的值
 * 由于已经将expOrFn转化成统一的形式,这里只需要合理的call一下
 */
get() {
    return this.getter.call(this.vm)
}

扩展后的Watcher已经观察一个计算过程,所以在处理computed属性时,将计算过程提取出来构造一个Watcher实例(后面叫它computedWatcher)。计算过程中用到的那些属性发生变化时,computedWatcher会执行update重新求值,此时应当通知观察了计算属性的那些Watcher。如果Watcher本身也可以观察,那么对计算属性的观察,就可以代理到computedWatcher上。

如果Watcher也能被观察

计算属性可以认为是一个Watcher实例的替身(通过闭包保持一致),因此对计算属性的求值可以借助Watcher的求值;对计算属性的观察,也可以认为是对Watcher的观察。那么Watcher需要一个集合来维护观察者们,并提供一个提交订阅的入口:

// 新增成员
watchers: Watcher[]

// 新增方法
addWatcher(watcher: Watcher) {
    this.watchers.push(watcher)
}

这样就可以让计算属性也被观察:

/**
 * 处理options中的computed部分
 */
initComputed() {
    let computed = this.$options.computed

    for (let key in computed) {
        const fn = computed[key]
        const computedWatcher = new Watcher(this, fn, noop)
        Object.defineProperty(this, key, {
            get() {
                Watcher.target && computedWatcher.addWatcher(Watcher.target)
                return computedWatcher.get()
            }
        })
    }
}

总结

Watcher的基本职责就是,观察一个目标,在目标发生变化时进行更新。而这个目标的概念,在前一篇文章中,是具体的单一状态;如果多个状态相互联系,但结果是单一的,那么这多个状态与单一的结果是等价的,而这个结果是多个状态相互联系的静态表达,这个“联系”才是动态的,有意义的。因此多个状态相互的关联,也可以被当做观察的目标。在此基础上扩展了Watcher的目标。

多个状态的关联,总是在任一状态变化时变化,计算属性就是关联结果。Watcher实例和观察目标之间是一一对应的,同时Watcher通过对目标求值提交注册,因此Watcher在一定程度上和观察目标是统一的。那么Watcher在观察单一状态时,本身就是一对一的关系,而观察多个状态的关系(计算过程)时,和计算结果一一对应,通过Watcher实例可以直达单一状态,或者多个状态的计算结果,而后者则成为computed计算属性的支撑。

通过computed属性指定了一个计算过程,也就是多个状态的相关关系(当然也有可能是单一状态的装饰过程),Watcher将这个计算过程包裹起来,与计算过程的结果直接对应,那么对于computed属性的访问,就是对Watcher实例的访问。而computed属性也具有响应式特征,遂在Watcher上也增加了订阅/通知的行为模式。

标签: none

添加新评论