超简单的Vue2响应式系统原理解释

902 阅读4分钟

所谓Vue.js的响应式系统也就是指,在修改数据模型的时候,视图会自动变化,而用户不需要像命令式编程那样再手动去操作视图的变化。用高大上的说法就是:使用订阅者模式,达成声明式编程的目的

Vue.js的响应式系统非常适合用订阅者模式的原因就在于,每次在修改数据的时候,对应通知到使用到该数据的组件,就实现了响应式系统。

那么Vue.js 是如何实现在每次数据改变的时候,自动通知到对应的组件的呢?

带着这个问题,我们来看下面的代码演示

代码只是拿来做展示原理的,真实的源代码复杂的多,但是原理相似

首先我们来定义两个类:

class Dep {
    constructor () {
        this.subs = [];
    }
    addSub (sub) {
        this.subs.push(sub);
    }
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

class Watcher {
    constructor () {
        Dep.target = this;
    }
    update () {
        // 执行更新视图的操作
    }
}

Dep 类是拿来存放订阅者的容器,它里面有个subs数组拿来存放有哪些订阅者,addSub(sub)就是添加对应的订阅者。notify()则是通知所有的订阅者,数据变化了。

Watcher 类是订阅者类(又叫观察者,两者是同样的意思)。里面的 update() 函数就是拿来做对应的视图更新,具体的在这里不展开。至于 Dep.target = this; 这行代码到后面再解释。

我们再来看看 Vue.js 是如何将数据模型变得响应的,

function defineReactive (obj, key, val) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            dep.addSub(Dep.target); // 添加订阅者
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            val = newVal;
            dep.notify();
        }
    });
}

这里应用到了原生 JavaScriptObject.defineProperty() 函数,如果对于这块不熟悉的,参考这里

defineReactive()函数返回的时候,会生成一个闭包,而这个闭包里面只有 dep 这一个属性,并且该闭包只能被 obj[key] 属性的 getset 方法所使用。dep 就是拿来存放 obj[key] 这个数据所对应的订阅者的。

你会发现,我们添加订阅者的操作就是在 get 方法中完成的,而为什么添加的订阅者是 Dep.target, 这点在后面解释。

set 函数就是拿来修改数据,并且使用 dep.notify(); 通知对应的订阅者。

这个函数是对让 Vue 实例中的 data 中的一个属性变得响应的,那么当然我们需要将 data 中的所有属性都变得响应式。这时候,就需要用到 observer() 函数了。

function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

observer() 这个函数很简单,接受对应的 data, 并将里面的属性都变得响应式。

好了,前面的基础都打好了,现在我们来看看 Vue 是如何在构造函数中,将整个响应式系统打造好的,其实也非常简单:

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        new Watcher();
        this.render(); // 执行render function,里面会获取到 this._data里面的数据,相当于执行了 data 里面属性的 'get' 方法,从而添加了该 Vue 实例的 Watcher 作为订阅者。
    }
}
let o = new Vue({
    data: {
        test: "I am test."
    }
});

我们先执行对应的observer()函数,将 data 里面的属性都变得响应式。

然后再生成一个 Watcher() 对象,这里就可以解释前面留下的问题,也就是为什么 Watcher 类的构造函数里面要执行 Dep.target = this; 的操作,也就是将 Dep.target 指向在当前 Vue实例中生成的 Watcher 对象;而我们留下的第二个问题,也就是为什么 dep.addSub(Dep.target); 添加的订阅者是 Dep.target, 其实也就是当前 Vue 实例中生成的 Watcher 对象。

但是这时 data 中的每个属性的订阅者其实还没有添加,所以我们要跑一次 render function 来获取一次数据,也就是调用 data 每个属性的 get 方法。

这里还有一个问题,那就是,如果 render function 被调用了多次,那么订阅者就会被添加多次,所以在最后还需要执行以下操作:

Dep.target = null;

你会发现,我们这里是对每个 Vue 实例(或者 Vue 组件)使用一个 Watcher 对象,这是Vue2.0的特性;而在Vue1.0 中是针对每个标签实现一个Watcher,数据过大时,就会有很多个watcher,会出现性能问题。