Vue响应式原理-Object的变化侦探

70 阅读3分钟

Object的变化侦探

ES6之前主要通过Object.defineProperty方法,ES6之后通过Proxy来实现对数据的侦查.

基于Object.defineProperty:对象的变化是靠setter来追踪的,一旦数据发生了变化,便会触发setter.例如下面的例子

Object.defineProperty(data ,key ,{
    value: 'tangytin'
    enumerable: true,
    configurable: true,
    get: function() {},
    // 当data.key发生变化,自动触发set,
    set: function(newVal) {},
})

当data.key的值发生变化的时候,会触发set, 可以得到变化后的值newVal。但是想要实现追踪数据变化,当然只是上面的代码远远不够,我们要考虑以下因素

  • 有哪些地方用到data.key,
  • 如何收集所有用到data.key的地方,
  • 如何触发每一处的data.key都发生变化

仔细想想以上两个因素,其实和我们之前提到的发布-订阅者模式很相像:但这种模式叫做观察者模式,那么观察者模式和发布-订阅者模式之间的区别,请看以下链接:developer.aliyun.com/article/499…

  • 收集data.key(订阅)
  • 触发data.key,获取变化后的值做一些事情(发布)

现在我们将追踪依赖的defineProperty封装起来,放入defineReactive函数中,并且给出一个数组dep来收集当前的data.key所有用到的地方(依赖),假设当前的依赖是一个函数(window.target),并且在data.key发生变化的时候触发所有的依赖项

function defineReactive(data, key, val) {
    // 依赖收集数组
    const dep = []
    Object.defineProperty(data ,key ,{
        enumerable: true,
        configurable: true,
        get: function() {
            dep.push(window.target)
            return val
        },
        // 当data.key发生变化,自动触发set,
        set: function(newVal) {
            for(let i = 0; i < dep.length; i++){
                dep[i](newVal,val)
            }
            if(newVal!==val) return newVal
        },
    })
}

这样一个简单的依赖收集就完成了,但是考虑到代码的耦合性,所以决定将收集依赖和依赖都单独放出来封装成一个类,我们用Dep来收集依赖,用Watcher来作为依赖的角色。

class Dep {
    constructor() {
        this.subs = []
    }
    
    addSub(sub) {
        this.subs.push(sub)
    }
    
    removeSub(sub) {
        if(this.subs.length) {
            const index = this.subs.indexOf(sub)
            if(index > -1)
                this.subs.splice(index,1)
        }
    }
    
    depend(sub) {
        if(window.target) {
            this.addSub(sub)
        }
    }
    
    notify() {
        const subs = this.subs.slice()
        for(let i = 0; i < subs.length; i++) {
            subs[i].update()
        }
    }
}

//同时对defineReactive进行相应的修改
function defineReactive(data, key, val) {
    // 依赖收集数组
    const dep = new Dep()
    Object.defineProperty(data ,key ,{
        enumerable: true,
        configurable: true,
        get: function() {
            dep.depend() //1
            return val
        },
        // 当data.key发生变化,自动触发set,
        set: function(newVal) {
            dep.notify() //2
            if(newVal!==val) return newVal
        },
    })
}

那么Watcher依赖是怎么来的呢,我所理解的依赖是当使用data.key的时候,就相当于订阅了

vm.$watch(key,cb)即:
vm.$watch('data.key', function(newVal, oldVal){
    //回掉做响应的处理
})

那么将以上内容抽象成类

class Watcher(){
    constructor(vm, expOrFn, cb){
        this.vm = vm
        // parsePath可以读取字符串的keyPath
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    
    get(){
        // 将自己(依赖)设置给window.target
        window.target = this
        // 触发getter,进行收集依赖
        let value = this.getter.call(this.vm, this.vm)
        //清空window.target
        window.target = undefined
        return value
    }
    
    // 触发依赖是,更新(调用回掉函数)
    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
    
}

现在就完美的实现了侦测数据中的某一个属性,如果我们想要侦测数据中的所有属性,还需要进行递归。将其封装进行一个Observer类

class Observer {
    constructor(value) {
        // 侦测的数据
        this.value = value
        //数组的侦测另作处理,下一章节介绍
        if(!Array.isArray(value)) {
            this.walk(value)
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj)
        for(let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
//同时对defineReactive进行相应的修改
function defineReactive(data, key, val) {
    // 依赖收集数组
    const dep = new Dep()
    // 递归
    if(typeof val === 'object') {
        new Observer(val)
    }
    // 侦测单个
    Object.defineProperty(data ,key ,{
        enumerable: true,
        configurable: true,
        get: function() {
            dep.depend()
            return val
        },
        // 当data.key发生变化,自动触发set,
        set: function(newVal) {
            dep.notify()
            if(newVal!==val) return newVal
        },
    })
}

总结

Object通过使用Object.defineProperty将属性转换成getter/setter来追踪变换,在getter中进行依赖收集,setter中触发通知收集的依赖发生变化