1、数据绑定(model==>View):
一旦更新了data中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新(更新)
2、数据劫持
①. 数据劫持是vue中用来实现数据绑定的一种技术
②. 基本思想: 通过defineProperty()来监视data中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
3、四个重要对象
①. Observer
②. Dep(Depend)
Dep{
id, // 每个dep都有一个唯一的id
subs //包含n个对应watcher的数组(subscribes的简写)
}
③. Compile
④. Watcher
Watcher {
vm, //vm对象
exp, //对应指令的表达式
cb, //当表达式所对应的数据发生改变的回调函数
value, //表达式当前的值
depIds //表达式中各级属性所对应的dep对象的集合对象
//属性名为dep的id, 属性值为dep
}
⑤. 总结: dep与watcher的关系: 多对多
4、双向数据绑定
①. 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
②. 双向数据绑定的实现流程:
observer.js:
function Observer(data) {
// 保存data
this.data = data;
// 启动对data对象中数据的劫持
this.walk(data);
}
Observer.prototype = {
walk: function(data) {
var me = this;
// 遍历data的所有属性
Object.keys(data).forEach(function(key) {
// 将data中属性重新定义的响应式
me.defineReactive(data, key, data[key])
});
},
defineReactive: function(data, key, val) {
// 创建一个对应的dep对象(订阅器/中间人)
var dep = new Dep();
// 通过隐式递归调用实现所有层次属性的监视/劫持
var childObj = observe(val);
// 给data重新定义属性, 添加setter/getter
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
// 用于建立dep与watcher的关系
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 偿试监视新的值的内部数据
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
// 创建一个对应的observer对象
return new Observer(value);
};
var uid = 0;
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
depend: function() {
Dep.target.addDep(this);
},
removeSub: function(sub) {
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
},
notify: function() {
// 遍历每个订阅者watcher
this.subs.forEach(function(sub) {
// 去更新对应的节点
sub.update();
});
}
};
Dep.target = null;
watch.js:
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.depIds = {};
// 读取当前表达式对应的属性值
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
// 调用绑定的更新节点的回调函数
this.cb.call(this.vm, value, oldVal);
}
},
addDep: function(dep) {
// 判断watcher与dep的关系是否已经建立过
if (!this.depIds.hasOwnProperty(dep.id)) {
// 将watcher添加dep中, 建立dep到watcher的关系
dep.addSub(this);
// 将dep添加到watcher中, 建立watcher到dep的关系
this.depIds[dep.id] = dep;
}
},
get: function() {
// 将当前watcher对象挂到Dep上
Dep.target = this;
// 读取表达式对应的属性值 ==> 调用对应的getter
var value = this.getVMVal();
Dep.target = null;
return value;
},
getVMVal: function() {
var exp = this.exp.split('.');
var val = this.vm._data;
exp.forEach(function(k) {
val = val[k];
});
return val;
}
};
首先,我们通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。
接着,我们学习了什么是依赖收集?并且知道了在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖。
最后,我们为每一个依赖都创建了一个Watcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作。
其整个流程大致如下:
Vue 的响应式是通过 Object.defineProperty 对数据进行劫持,并结合观察者模式实现。
Vue 创建了一个 observe 来劫持监听所有的属性,通过遍历,把这些属性包括子属性全部添加了 getter 和 setter。
Vue 它会在组件渲染的过程中(模板解析)把使用过的data属性所对用的 watcher 通过 getter 收集到 Dep 中,作为依赖。之后当数据发生了变化时,触发setter,会调用 dep 通知 所对应的 watcher,然后 watcher 调用更新的回调函数来更新节点。
触发Watch对象的update实现
当异步执行update的时候,会调用queueWatcher函数。异步推送到观察者队列中,下一个tick时调用
Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等待下一个tick时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去,因为在最终渲染时,我们只需要关心数据的最终结果。
虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新,需要手动进行 Observe,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
所以,在使用 Vue 给 data 中的数组或对象新增 / 删除属性时,需要使用 vm.$ set / vm.$ delete才能保证新增 / 删除的属性也是响应式的。
Array型数据设计一套另外的变化侦测机制。
由于数组arr的索引值恰好就是arrObj的key值,所以我们通过数组的索引值来操作数组时是可以用Object.defineProperty监测到的。但是,数组并不是只能由索引值来操作数组,更常用的操作数组的方法是使用数组原型上的一些方法如(push,pop,shift,unshift,splice,sort,reverse)
等来操作数组,当使用这些数组原型方法来操作数组时,Object.defineProperty就监测不到了,所以Vue对Array型数据单独设计了数据监测方式。
Array型数据还是在getter中收集依赖。
在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,并将这些方法赋值给了数据的 __ proto __ 上,因为原型链的机制,找到对应的方法就不会继续往上找原生的方法了,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。
拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中调用 dep 通知 所对应的 watcher。
编译方法中会对一些会增加索引的方法(push,unshift,splice)进行手动 observe。
在 Array 中,用拦截器代替了setter。