vue如何实现Object的变化检测

965 阅读4分钟

如何追踪变化

追踪变化有两种方式:Object.defineProperty和Proxy。由于ES6在浏览器中的支持问题,vuejs在3.0之前还是使用Object.defineProperty来实现变化检测,在3.0中会用Proxy改写这部分。

首先封装Object.defineProperty:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val
        },
        set: function(newVal) {
            if(val === newVal) {
                return
            }
            val = newVal;
        }
    })
}

封装好后,每当从data的key中读取数据时,get函数被触发,每当网data的可以中设置数据时, set函数被触发

如何收集依赖

只是封装Object.defineReactive毫无意义,我们需要收集依赖,那么什么是依赖呢?我们之所以要观察数据,是为了在数据的属性发生变化时可以通知哪些使用了该数据的地方。而依赖就是使用了数据的地方。在getter中收集依赖,在setter中触发依赖。 我们在每个key上设置一个数组,用来储存当前key的依赖,我们先假设依赖保存在一个叫target的全局变量上。改造一下defineReactive函数:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        let dep = []; // 新增
        enumerable: true,
        configurable: true,
        get: function() {
            dep.push(window.target) // 新增
            return val
        },
        set: function(newVal) {
            if(val === newVal) {
                return
            }
            for(let i = 0; i < dep.length; i ++) {
                dep[i](newVal, val); // 触发依赖
            }
            val = newVal;
        }
    })
}

这里我们新增了数组dep,用来储存被收集的依赖,然后当set被触发时,循环dep触发我们收集到的所有依赖。

依赖是谁

上文中我们使用全局变量target作为依赖保存的地方,那它到底是什么呢?或者说,我们到底在收集谁呢?我们要收集用到这个数据的地方,而使用这个数据的地方很多,而且类型不一样,即可能是模板里面,也可能在watch里面。我们可以抽象出一个能集中处理这些情况的类,getter和setter也只通过它一个。 下面我们定义一个watcher的类来实现以上功能。实现思路是将watcher添加到属性的dep中即可

export default class Watcher {
    constructor (vm. expOrFn. cb) {
        this.vm = vm;
        // 执行this.getter()即可读取属性的内容,稍后介绍parsePath的写法
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get()
    }
    get() {
        window.target = this;
        let value = this.getter.call(this.vm, this.vm);
        window.target = undifined
        return value;
    }
    update() {
        const oldVale = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this,value, oldValue);
    }
}

以上代码可以将自己主动添加到对象属性的dep中去。

代码解说: 首先将window.target设置成this,也就是当前watcher实例。然后再读一下对象属性的值,就肯定会触发getter.触发getter就会触发收集依赖的逻辑,即从window.target中读取一个依赖并添加到dep中,这就导致只要先在window.targe赋予一个this,然后在读一下值,去触发getter,就可以吧this主动添加到keypath的dep中。

依赖注入到dep中后,每当属性值发生改变时,就会循环触发update方法,也就是watcher中的update方法!

下面简要讲一下parsePath的实现:

const bailRe = /[^\e.$]/;
export function parsePath(path) {
    if(bailRe.test(path)){
        return 
    }
    const segments = path.split('.');
    return funcion(obj) {
        for (let i = 0; i < segments.length; i ++) {
            if(!obj) return;
            obj = obj[segements[i]]
        }
        return obj
    }
}

其实只是现将keypath用.分隔成数组,然后循环一层层读取数据,最后拿到的就是keypath中想要读的数据。

递归侦测所有key

以上已经基本实现变化侦测的功能了,但签名所介绍的代码只能侦测数据中的某一个属性,现在我们想要将数据中所有的属性(包括子属性)都能侦测到,那就要封装一个Observer类,这个类的作用就是将一个数据内所有的属性都转换成getter/setter的形式, 然后去追踪他们的变化。

export class observer {
    constructor(value) {
        this.value = value;
        if(!Array.isArray(value)) {
            this.walk(value)
        }
    }
    /**
    * walk会将每个属性转换成getter/setter的形式来侦测变化。
    * 这个方法只有在数据类型为Object的时候被调用
    */
    walk(obj) {
        const keys = Object.keys(obj);
        for(let i = 0; i < keys.length; i ++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    }
}
function defineReactive(data, key,val) {
    // 新增, 递归子属性
    if(typeof val === "object") {
        new Observer(val)
    }
    let dep = []; // 新增
        enumerable: true,
        configurable: true,
        get: function() {
            dep.push(window.target) // 新增
            return val
        },
        set: function(newVal) {
            if(val === newVal) {
                return
            }
            for(let i = 0; i < dep.length; i ++) {
                dep[i](newVal, val); // 触发依赖
            }
            val = newVal;
        }
}

在上面的代码中,我们定义了Observer类,它用来将正常的object转化成被侦测的object。只要我们将一个object传入到Observer中,那么该对象就会变成响应式的object。

关于Object的问题

上面介绍了Object类型数据的变化侦测原理,了解了数据的变化是通过getter/setter来最终的,也是由于这种方式,有些数据即使发生了变化,vuejs也追踪不到。比如为对象增加或删除属性时,因为vuejs是通过Object.defineProperty将对象的key转换成getter和setter形式来追踪变化的,但是这样的话只能追踪数据是否被修改,无法追踪新增与删除属性。所以vuejs提供了vm.set和vm.delete这两个api来解决这个问题。

结语

以上内容参考《深入浅出Vue.js》一书。如有错误之处,敬请斧正。