响应式
响应式是指状态的(对象的属性)修改之后,能自动更新到视图上。这样我们就可以集中精力在具体的业务操作上,并不需要关心dom。
vue主要是通过:数据劫持 + 依赖收集(这里用到了发布者-订阅者模式)的方式来实现的。
第一是:数据劫持,又叫数据拦截。vue2是通过Object.defineProperty来将对象的每一个属性转化成setter,getter。其中修改对象的属性时,就会触发setter,这样可以知道哪个属性被修改了。
第二是:依赖收集。就在渲染视图时,将 Watcher和具体的属性,通过发布者订阅者模式管理起来,这样数据改变之后,就能精准更新视图。
收集依赖
vue的整个工作过程
- 挂载前。对data对象进行遍历生成对应的setter,getter。在getter中维护了一个用来收集依赖的对象。
- 挂载时
- 把template/el 转成 render函数
- 创建updateComponent函数,在这个函数中会调用render函数
- 给每个组件/vue实例 new Watcher,并传入updateComponent函数
- 在构造器内,调用一次updateComponent
- 当watcher调用时,就会调用updateComponent函数
- 挂载后
- 用户更新数据就触发对应的watcher,再次更新视图
收集依赖的过程就发生的new watcher的构造器内,它会调用render函数,而render会访问data中的属性,进而触发getter,在getter内部就完成了依赖收集
每new一个watcher时,就会给Watcher添加一个ID,这样在添加依赖的会先判断这个ID是否有重复的,以免重复收集。
特殊处理数组的响应式
Vue2.0内部并没有直接逐一去给数组元素添加响应式功能:如果你直接通过下标去向数组中传入新值,vue并不能监测到。其实Object.defineProperty是可以监控对数组的修改的,如下代码:
var arr = [1,2]
for(var k in arr){
console.log(k)
Object.defineProperty(arr, k, {
getter() {
return arr[k]
},
setter(newVal) {
if(newVal !== arr[k]){
return
}
console.log('setter ....', newVal)
}
})
}
arr[0] = 100 // 能触发setter
vue2.0之所以没有这么做是因为数组元素的个数是不可控的(想象一下1000个元素的数组),并且可以随时增减的。
vue2.0中,对于数组的处理是拦截了数组上的7个方法push,pop,shift,unshift,splice,sort,reverse,也就是说只有调用如上7个方法中的某一个才能触发响应式。
为啥是这7个呢? 因为只有它们才会修改源数组
核心代码如下:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
以拦截push方法为例,其简化理解代码如下:
const arrayMethods = Object.create(Array.prototype)
Object.defineProperty(arrayMethods,'push', {
value:function(...arg){
const res = Array.prototype.push.apply(this,arg) // 执行原来的push
// 1. 对新数据做响应式处理
console.log('对新数据做响应式处理')
// 2. 通知观察者们去执行
console.log('通知观察者们去执行')
return res
}
})
var arr = [1,2,3]
arr.__proto__ = arrayMethods
arr.push(4)
数组的操作替代
问题1: 通过下标直接修改不能响应式
2种方式都可以实现下标修改数据还能响应式: (1) Vue.set,或者this.$set
// Vue.set
Vue.set(this.arr, idx, 新值)
(2) 借用splice方法:删除一元素,接着添加一个元素
// Array.prototype.splice
this.arr.splice(indexOfItem, 1, newValue)
问题2: 直接修改length属性没有响应式
可以使用 splice:
this.arr.splice(你要设置的数组长度)