所谓Vue.js的响应式系统也就是指,在修改数据模型的时候,视图会自动变化,而用户不须要像命令式编程那样再手动去操做视图的变化。用高大上的说法就是:使用订阅者模式,达成声明式编程的目的。javascript
Vue.js的响应式系统很是适合用订阅者模式的缘由就在于,每次在修改数据的时候,对应通知到使用到该数据的组件,就实现了响应式系统。java
那么Vue.js 是如何实如今每次数据改变的时候,自动通知到对应的组件的呢?react
带着这个问题,咱们来看下面的代码演示编程
代码只是拿来作展现原理的,真实的源代码复杂的多,可是原理类似数组
首先咱们来定义两个类:闭包
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 () {
console.log("视图更新啦~");
}
}
复制代码
Dep
类是拿来存放订阅者的容器,它里面有个subs
数组拿来存放有哪些订阅者,addSub(sub)
就是添加对应的订阅者。notify()
则是通知全部的订阅者,数据变化了。函数
Watcher
类是订阅者类(又叫观察者,二者是一样的意思)。里面的 update()
函数就是拿来作对应的视图更新,具体的在这里不展开。至于 Dep.target = this;
这行代码到后面再解释。性能
咱们再来看看 Vue.js
是如何将数据模型变得响应的,ui
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()
函数,若是对于这块不熟悉的,参考这里this
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();
console.log('render~', this._data.test);
}
}
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
方法。console.log('render~', this._data.test);
是拿来模拟 render function
的调用的。
这里还有一个问题,那就是,若是 render function
被调用了屡次,那么订阅者就会被添加屡次,因此在最后还须要执行如下操做:
Dep.target = null;
复制代码
你会发现,咱们这里是对每一个 Vue
实例(或者 Vue 组件)使用一个 Watcher
对象,这是Vue2.0的特性;而在Vue1.0 中是针对每一个标签实现一个Watcher
,数据过大时,就会有不少个watcher,会出现性能问题。