监听数据对象变化,最容易想到的是建立一个需要监视对象的表,定时扫描其值,有变化,则执行相应操作,不过这种实现方式,性能是个问题,如果需要监视的数据量大的话,每扫描一次全部的对象,需要的时间很长。当然,有些框架是采用的这种方式,不过他们用非常巧妙的算法提升性能,这不在我们的讨论范围之类。
Vue 中数据对象的监视,是通过设置 ES5 的新特性(ES7 都快出来了,ES5 的东西倒也真称不得新)Object.defineProperty() 中的 set、get 来实现的。
目标
与官方文档第一个例子相似,不过也有简化,因为这篇只是介绍下数据对象的监听,不涉及文本解析,所以文本解析相关的直接舍弃了:
<div id="app"></div>
var app = new Vue({
el: 'app',
data: {
message: 'Hello Vue!'
}
});
</div>
浏览器显示:
Hello Vue!
在控制台输入诸如:
app.message = 'Changed!'
之类的命令,浏览器显示内容会跟着修改。
Object.defineProperty
引用 MDN 上的定义:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
与此相生相伴的还有一个 Object.getOwnPropertyDescriptor():
Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
下面的例子用一种比较简单、直观的方式来设置 setter、getter:
var dep = [];
function defineReactive(obj, key, val) {
// 有自定义的 property,则用自定义的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
dep.push(value);
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值与原值相同,则不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
console.log(dep);
}
});
}
</div>
var a = {};
defineReactive(a, 'a', 12);
// 调用 getter,12 被压入 dep,此时 dep 值为 [12]
a.a;
// 调用 setter,输出 dep ([12])
a.a = 24;
// 调用 getter,24 被压入 dep,此时 dep 值为 [12, 24]
a.a;
</div>
Observer
简单说过 Object.defineProperty 之后,就要开始扯 Observer 了。observer,中文解释为“观察者”,观察什么东西呢?观察对象属性值的变化。故此,所谓 observer,就是给对象的所有属性加上 getter、setter,如果对象的属性还有属性,比如说 {a: {a: {a: 'a'}}},则通过递归给其属性的属性也加上 getter、setter:
function Observer(value) {
this.value = value;
this.walk(value);
}
Observer.prototype.walk = function(obj) {
var keys = Object.keys(obj);
for(var i = 0; i < keys.length; i++) {
// 给所有属性添加 getter、setter
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
var dep = [];
function defineReactive(obj, key, val) {
// 有自定义的 property,则用自定义的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
// 递归的方式实现给属性的属性添加 getter、setter
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
dep.push(value);
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值与原值相同,则不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 给新赋值的属性值的属性添加 getter、setter
childOb = observe(newVal);
console.log(dep);
}
});
}
function observe(value) {
if(!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
</div>
Watcher
Observer 通过设置数据对象的 getter、setter 来达到监听数据变化的目的。数据被获取,被设置、被修改,都能监听到,且能做出相应的动作。
现在还有一个问题就是,谁让你监听的?
这个发出指令的就是 Watcher,只有 Watcher 获取数据才触发相应的操作;同样,修改数据时,也只执行 Watcher 相关操作。
那如何讲 Observer、Watcher 两者关联起来呢?全局变量!这个全局变量,只有 Watcher 才做修改,Observer 只是读取判断,根据这个全局变量的值不同而判断是否 Watcher 对数据进行读取,这个全局变量可以附加在 dep 上:
dep.target = null;
根据以上所述,简单整理下,代码如下:
function Watcher(data, exp, cb) {
this.data = data;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype.get = function() {
// 给 dep.target 置值,告诉 Observer 这是 Watcher 调用的 getter
dep.target = this;
// 调用 getter,触发相应响应
var value = this.data[this.exp];
// dep.target 还原
dep.target = null;
return value;
};
Watcher.prototype.update = function() {
this.cb();
};
function Observer(value) {
this.value = value;
this.walk(value);
}
Observer.prototype.walk = function(obj) {
var keys = Object.keys(obj);
for(var i = 0; i < keys.length; i++) {
// 给所有属性添加 getter、setter
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
var dep = [];
dep.target = null;
function defineReactive(obj, key, val) {
// 有自定义的 property,则用自定义的 property
var property = Object.getOwnPropertyDescriptor(obj, key);
if(property && property.configurable === false) {
return;
}
var getter = property && property.get;
var setter = property && property.set;
// 递归的方式实现给属性的属性添加 getter、setter
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = getter ? getter.call(obj) : val;
// 如果是 Watcher 监听的,就把 Watcher 对象压入 dep
if(dep.target) {
dep.push(dep.target);
}
return value;
},
set: function(newVal) {
var value = getter ? getter.call(obj) : val;
// set 值与原值相同,则不更新
if(newVal === value) {
return;
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 给新赋值的属性值的属性添加 getter、setter
childOb = observe(newVal);
// 按序执行 dep 中元素的 update 方法
for(var i = 0; i < dep.length; i++) {
dep[i].update();
}
}
});
}
function observe(value) {
if(!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
</div>
var data = {a: 1};
new Observer(data);
new Watcher(data, 'a', function(){console.log('it works')});
data.a =12;
data.a =14;
</div>
上面基本实现了数据的监听,bug 肯定有不少,不过只是一个粗糙的 demo,只是想展示一个大概的流程,没有扣到非常细致。
Dep
上面几个例子,dep 是个全局的数组,但凡 new 一个 Watcher,dep 中就要多一个 Watcher 实例,这时候不管哪个 data 更新,所有的 Watcher 实例的 update 都会执行,这是不可接受的。
Dep 抽象出来,单独搞一个构造函数,不放在全局,就能解决了:
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
};
Dep.prototype.notify = function() {
var subs = this.s

