所谓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();
}
});
}
这里应用到了原生 JavaScript
的 Object.defineProperty()
函数,如果对于这块不熟悉的,参考这里
defineReactive()
函数返回的时候,会生成一个闭包,而这个闭包里面只有 dep
这一个属性,并且该闭包只能被 obj[key]
属性的 get
和 set
方法所使用。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,会出现性能问题。