一、Vue2是如何实现数据响应式的
Vue
实现响应式变化的方式是通过数据劫持和发布-订阅模式。
-
数据劫持:
Vue
通过使用Object.defineProperty()
方法对数据对象的属性进行劫持,使其能够在属性值发生变化时触发相应的操作。 -
发布-订阅模式:
Vue
使用发布-订阅模式来监听数据变化,并在变化时通知相关的订阅者更新视图。当数据发生变化时,会触发相应的setter方法,然后通知所有订阅者进行更新。
具体实现步骤如下:
-
在初始化
Vue
实例时,通过遍历数据对象,利用Object.defineProperty()
方法将每个属性转化为getter
和setter
。 -
在
getter
方法中,将订阅者Watcher
对象添加到当前属性的依赖列表中。 -
在
setter
方法中,当数据发生改变时,会触发该属性的所有订阅者的更新操作,即将Watcher
对象的update
方法加入到异步更新队列中。 -
当所有同步任务执行完毕后,异步更新队列会依次执行各个
Watcher
对象的update
方法,更新视图。
通过数据劫持和发布-订阅模式的结合,Vue
能够在数据变化时及时更新视图,实现了响应式变化。
export function defineReactive ( // 定义响应式数据
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// 如果不可以配置直接return
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 对数据进行观测
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { // 取数据时进行依赖收集
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) { // 让对象本身进行依赖收集
childOb.dep.depend() // {a:1} => {} 外层对象
if (Array.isArray(value)) { // 如果是数组 {arr:[[],[]]}
vm.arr取值只会让arr属性和外层数组进行收集
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {return}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
二、vue2中如何检测数组的变化
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择重写
数组( push,shift,pop,splice,unshift,sort,reverse )方法。
数组中如果是对象数据类型也会进行递归劫持
数组的索引和长度变化是无法监控到的
检测数组的变化是通过重写数组原型上的相关方法来实现的。具体步骤如下:
-
首先,Vue会判断当前浏览器是否支持原生的数组方法。如果支持,则直接重写数组原型上的方法,并在重写的方法中添加对应的变异方法。如果不支持,则创建一个新的对象,使用Object.defineProperty来拦截数组变异方法。
-
在重写数组方法时,Vue会先缓存原生的数组方法,比如
Array.prototype.push
、Array.prototype.pop
等。然后,在重写的方法中,先调用缓存的原生方法,再根据不同的变异方法类型执行不同的操作,比如添加响应式元素、触发依赖更新等。 -
Vue还会判断是否支持
__proto__
属性,如果支持,则直接将数组的原型指向重写的原型对象,以便数组的原型链能够正常查找到重写的方法。如果不支持,则遍历重写的原型对象,将其中的方法拷贝到数组的实例上。
示例代码如下:
// 是否支持原生数组方法
const hasProto = '__proto__' in {};
// 缓存原生数组方法
const arrayProto = Array.prototype;
// 创建重写的原型对象
const arrayMethods = Object.create(arrayProto);
// 定义重写的原型方法
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
// 缓存原生数组方法
const original = arrayProto[method];
// 重写原型方法
def(arrayMethods, method, function mutator(...args) {
// 调用原生数组方法
const result = original.apply(this, args);
// 获取当前数组的__ob__属性,即Observer实例
const ob = this.__ob__;
// 数组变异操作的类型
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 如果有新增的元素,将其转换为响应式对象
if (inserted) ob.observeArray(inserted);
// 触发依赖更新
ob.dep.notify();
return result;
});
});
// 将重写的原型对象设置到数组的原型上
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
if (hasProto) {
protoAugment(target, arrayMethods);
} else {
copyAugment(target, arrayMethods, arrayKeys);
}
通过以上代码,Vue实现了对数组变化的检测,并能够自动追踪数组的操作,以实现响应式更新。