Vue2
响应式数据
Vue2的对象数据中数据响应式是通过Object.defineProperty
将对象上的数据属性替换成访问器属性
Object.defineProperty
Object.defineProperty(obj, key, desc);
obj
: 需要定义属性的当前对象key
: 定义的属性名desc
: 属性描述符
示例
const girl = {};
Object.defineProperty(girl, 'sex', {
value: '女'
})
console.log(girl); // { sex: '女' }
这样就在girl
这个对象上添加了sex
属性,这样写与girl.sex = '女'
无异,定义的都是数据属性
let girl = {}, val = '女';
Object.defineProperty(girl, 'sex', {
get() {
return val;
},
set(newVal) {
val = newVal;
},
})
console.log(girl);
通过getter
和setter
方法在该对象上定义了一个访问器属性,当获取该属性时就会执行getter
方法,修改该属性时就会执行setter
方法
在浏览器的
DevTools
中会这样显示{ sex: (...) }
,点击...
就会去访问getter
方法,然后得到该属性的值
依赖收集流程
Observer
实例通过Object.defineProperty()
方法劫持data
上的属性,重新定义为访问器属性,同时并创建一个Dep
实例,与data
上的属性一一对应- 当
Watcher
实例访问data
上的属性时会触发对应属性的getter
方法,getter
方法会再调用dep.depend()
方法将该Watcher
实例添加到与该属性对应的Dep
实例的subs
数组中,来收集依赖
- 当数据被修改,触发
setter
方法,setter
方法会再调用dep.notify()
来通知被修改的属性对应的Dep
的subs
数组中所有的Watcher
进行更新操作
Observer类
方法
constructor
- 创建一个
Dep
对象,该对象用于数组的收集依赖和通知更新。 - 通过
Object.defineProperty
在数据对象上添加一个不可枚举的__ob__
属性,其值为当前 Observer 实例,防止重复观察同一数据对象。 - 判断是否是数组类型,如果是数组则调用
observeArray()
方法,否则调用walk()
方法
constructor(data) {
// 因为Observer劫持的data一定是一个对象,每个对象身上又挂载了__ob__(该Observer实例)
// 如此就可以通过__ob__访问到该Dep对象
// 是为了当向数组或对象中添加数据时候能够依赖收集
this.dep = new Dep();
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false
})
if (Array.isArray(data)) {
Object.setPrototypeOf(data, arrayMethods)
this.observeArray(data);
} else {
this.walk(data);
}
}
walk()
遍历data
上的属性并调用defineReactive()
将属性定义为访问器属性
walk(data) {
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
observeArray
遍历数组,劫持该数组
// Observer constructor():
// if (Array.isArray(data)) {
// Object.setPrototypeOf(data, arrayMethods)
// this.observeArray(data);
// }
const arrayProto = Array.prototype; // 获取Array构造函数的原型
export let arrayMethods = Object.create(arrayProto); 拷贝一份Array的原型对象
const methodsToPatch = [ // 将会修改数组的方法存入一个数组内
'push',
'pop',
'shift',
'unshift',
'spilce',
'sort',
'reverse'
]
methodsToPatch.forEach(method => {
arrayMethods[method] = function(...args) {
const result = arrayProto[method].call(this, ...args);
let inserted, ob = this.__ob__;
switch(method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
inserted && ob.observeArray(inserted); // 劫持新添加的数组
ob.dep.notify(); // 调用Observer实例上的Dep实例的notify方法通知更新
return result;
}
})
observeArray(data) {
data.forEach(item => observe(item));
}
...
defineReavtive
- 创建一个
Dep
对象,该对象在getter
和setter
中调用形成了一个闭包,每一个数据都对应一个Dep
对象 - 定义一个
getter
方法,在getter
中调用dep.depend()
方法将当前Watcher
实例添加到与该属性对应的Dep
实例的subs
数组中,来收集依赖
- 定义一个
setter
方法,在setter
中调用dep.notify()
来通知被修改的属性对应的Dep
的subs
数组中所有的Watcher
进行更新操作
export function defineReactive(obj, key, val) {
const dep = new Dep(); // 在getter和setter中访问dep变量形成了一个闭包
Object.defineProperty(obj, key, {
get() {
dep.depend(); // 收集依赖,将Watcher放入subs数组中
return val; // 返回值
},
set(newVal) {
observe(newVal); // 继续劫持新值
val = newVal; // 设置新值
dep.notify(); // 派发更新
}
})
}
observe
劫持一个对象
export function observe(value) {
// 如果不是对象则直接返回
if (typeof value !== 'object' || value == null) return;
// 如果已经有Observer实例,则直接返回
if (value && value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__;
}
// 创建实例观察数据
return new Observer(value);
}
在
Observer
实例上创建的Dep
实例是为了收集对象(数组)的依赖,而在defineReactive
上创建Dep
实例是为了收集数据的依赖
Dep类
方法
constructor
constructor() {
this.subs = [];
}
addSub
将一个Watcher
实例添加到subs
数组中
addSub(sub) {
this.subs.push(sub);
}
removeSub
从subs
数组中移除一个Watcher
实例
removeSub(sub) {
this.subs[this.subs.indexOf(sub)] = null;
}
depend
depend() {
if (Dep.target) {
this.addSub(Dep.target); // Dep.target是Dep构造函数上的一个属性
}
}
notify
const subs = this.subs.slice(); // 复制一份
subs.forEach(sub => sub.update()); // 通知更新
...
Watcher类
方法
constructor
constructor(vm, expr, cb, options) {
this.vm = vm // Vue实例
this.expr = expr; // 监听的表达式或方法 例: user.name
this.cb = cb; // 回调函数
this.getter = parsePath(expr); // 把路径解析成对象
this.options = options; // 是否是一个渲染Watcher
}
get
get() {
Dep.target = this; // 将this指向当前的Watcher实例
const vm = this.vm;
let val;
try {
val = this.getter.call(vm, vm); // 执行parsePath(expr)
// export default function parsePath(path) {
// const segments = path.split('.');
// return function(obj) {
// if (!obj) return;
// for (let i = 0; i < segments.length; i++) {
// obj = obj[segments[i]];
// }
// return obj;
// }
// }
// parsePath是传入一个路径,返回一个函数,这个函数接收一个对象作为参数,然后通过这个路径从对象中取出值,并返回这个值
// parsePath('user.name')({user: {name: 'jack'}}) // jack
} finally {
Dep.target = null; // 将Dep.target重置为null
}
return val;
}
...
总结
!. 出于对性能的考虑,Vue 没有对数组类型的数据使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持,直接通过数组索引来设置元素时,Vue 2不能直接检测到变化。例如,arr[index] = value 这样的操作不会触发视图更新。为了解决这个问题,Vue 2 提供了一组特殊的数组方法,如 $set
、$delete
,用于对数组进行修改并触发视图更新。
依赖收集的目的是建立从数据到依赖它的 Watcher 实例之间的关系,以便在数据发生变化时,通知相关的 Watcher 执行更新操作。