Vue源码学习-Object数据响应

1,103 阅读2分钟

导言

其实,在Vue中 Object 和 Array 的变化监听是采用不同的处理方式的

Object 的变化监听

如何追踪变化

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 的 key 中设置数据时 , set函数被触发

如何收集依赖

我们之所以要观察数据,其目的是当数据的属性发生变化时, 可以通知那些曾经使用了该数据的地方.

<template>
    <h1>{{name}</h1>
</template>

先收集依赖,即把用到的数据 name 地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。 在getter 中收集依赖,在setter 中触发依赖

依赖收集到哪里

我们要在 getter 中收集依赖, 那么要把依赖收集到哪里去呢?
我们把 defineReactive 函数稍微改造一下

function defineReactive(data, key , val) {
    let dep = [];
    Object.defineProperty(data, key, {
        configurable : true,
        enumerable : 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 类,我们可以收集依赖,删除依赖,或者向依赖发送通知

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

之后再改造一下 defineReactive

function defineReactive(data, key , val) {
    let dep = new Dep();
    Object.defineProperty(data , key , {
        enumerable : true,
        configurable : true,
        get : function() {
            dep.depend(); // 收集依赖
            return val;
        },
        set : function(newVal) {
            if(val === newVal) {
                return;
            }
            val = newVal;
            dep.notify(); // 触发依赖
        }
    })
}

依赖是什么

在上面的代码中,我们收集的依赖是 window.target, 那么它到底是什么呢?
换句话说,就是当属性发生变化后,通知谁?
我们要通知用到的数据的地方, 而使用数据的地方很多

  • 模板
  • 用户写的一个 watch

我们需要抽象出一个集中处理这些情况的类,就叫做 Watcher

什么是 Watcher

Watcher 是一个中介的角色, 数据发生变化时通知它 , 然后它再通知其他地方。 关于 Watcher , 先看一个经典的使用方式:

vm.$watch('count', function(newVal, val) {
    
})

思考一下,怎么实现这个功能?

class Watcher {
    constructor(vm, expOrFn , cb) {
        this.vm = vm;
        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 = undefined;
        return value;
    }
    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue)
    }
}

递归检测所有key

其实,我们已经可以实现变化检测的功能了, 但是前面介绍的代码只能检测数据中的某一个属性,我们希望把数据中的所有属性(包括子属性)都检测到, 所以要封装一个 Observer 类。 这个类的作用是将一个数据内的所有属性(包括子属性)都转换成 getter/setter 的形式, 然后去追踪他们的变化。

 class Observer {
    constructor(value) {
        this.value = value;
        if(Array.isArray(value)) {
            this.walk(value)
        }
    }
    /**
     * 这个方法只有数据类型为 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 = new Dep();
    Object.defineProperty(data, key , {
        enumerable : true,
        configurable : true,
        get : function() {
            dep.depend();
            return val;
        },
        set : function(newVal) {
            if(val === newVal) {
                return;
            }
            val = newVal;
            dep.notify();
        }
    })
}

关于 Object.defineProperty 的缺陷问题

前面介绍了 Object类型数据的变化检测原理 , 了解了数据的变化是通过 Object.defineProperty 来追踪的。但也正是这种追踪方式, 有些语法中即便数据发生了变化,vue.js也追踪不到
就比如这样:

var vm = new Vue({
    el : '#el',
    methods : {
        action () {
            this.obj.name = 'zs'
        }
    },
    data : {
        obj : {}
    }
})

在上面的代码中, 我们在 action 方法中删除了 obj 中的 name 属性, 而 vue.js 无法检测到这个变化, 所以不会向依赖发送通知。
为了解决这个问题,vue.js提供了两个API

  • vm.$set
  • vm.$delete