变化侦测指的是对数据进行监听当数据发生变化时,对变化的数据重新渲染,实现响应式渲染。在Vue中object和array的变化侦测是不一样的,这一篇主要介绍object的变化侦测。
object的变化侦测的核心是监听object数据的变化,可以通过Object.defineProperty和Proxy实现,Vue2.x时,ES6支持不是很理想,采用的是Obejct.definedProperty,Vue3已经使用Proxy对数据侦测进行重写。这一篇主要以Vue2.x为例介绍其原理,虽然Vue3使用的是Proxy,但是原理都一样。
object变化侦测的流程是当对象触发getter时对依赖进行收集,当对象触发setter时触发与该数据相关的所有依赖,就是通知所有使用了该数据的地方。总结为一句话就是:getter收集依赖,setter触发依赖。
既然知道了是使用Object.defineProperty可以侦听到对象的变化,那么就封装一个用于侦听对象变化的函数。
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
},
get() {
return val;
}
});
}
不过只是侦测对象的变化还不够,还得需要在数据变化时,做一些其他的处理,才能让数据动态显示。接下来对该函数做一些改造。
function defineReactive(data, key, val) {
let dep = [];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
set(newVal) {
if (val === newVal) {
return;
}
for(let i = 0; i < dep.length; i++){
dep[i](newVal, val);
}
val = newVal;
},
get() {
dep.push(function(){});
return val;
}
});
}
这样就可以在数变化的时候在dep中的函数执行自定义逻辑。接下来,进行一些优化,把自定义的逻辑回调函数,分离出来,单独进行管理。
这个Dep类专门用来管理回调函数。
class Dep {
constructor() {
// 用来存储回调函数
this.subs = [];
}
// 添加回调函数辅助函数
addSub(sub) {
this.subs.push(sub);
}
// 追加回调函数
dependSub() {
// 为undefined时不再添加,否则会一直添加导致死循环
if (window.target) {
// 添加固定的属性,值可以改变
this.addSub(window.target);
}
}
// 移除回调函数
removeSub(sub) {
this.remove(this.subs, sub);
}
// 执行回调函数
notify() {
let subs = this.subs.slice(0);
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
}
// 移除回调函数辅助函数
function remove(subs, sub) {
for (let i = 0; i < subs.length; i++) {
if (sub === subs[i]) {
subs.splice(i, 1);
return;
}
}
}
然后对defineReactive函数进行改造
function defineReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify();
},
get() {
dep.dependSub();
return val;
}
});
}
接下来对需要收集的依赖进行管理。依赖就是Watcher。Watcher先存储在全局指定的位置,然后读取数据,触发getter操作收集依赖,哪个Watcher触发getter就把哪个Watcher存储在Dep中,在触发getter时,将所有的Watcher都通知一遍。
class Watcher {
constructor(vm, expOrFn, cb) {
// vm实例
this.vm = vm;
// 存储用于触发对象getter操作的函数,当执行getter时会访问侦测对象中的属性,以触发对象的getter操作,收集依赖
this.getter = parsePath(expOrFn);
// 存储回调函数
this.cb = cb;
// 存储属性的当前值
this.value = this.get();
}
// 通过访问对象属性,触发getter操作,开始收集依赖
get() {
window.target = this;
let value = this.getter.call(this.vm, this.vm);
// 手动设置为undefined,用于是否继续添加依赖的判断
window.target = undefined;
return value;
}
// 当侦测到对象数据变化会执行该函数
update() {
let oldValue = this.value;
// 存储对象的新值,同时重新收集依赖
this.value = this.get();
// 执行回调函数
this.cb.call(this.vm, this.value, oldValue);
}
}
// 解析变量简单访问路径
function parsePath(path) {
let segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]];
}
return obj;
}
}
接下来再对对象侦测进行封装。
class Observer {
constructor(value) {
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(data) {
let keys = Object.keys(data);
// 监听对象的所有属性
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i], data[keys[i]]);
}
}
}
function defineReactive(data, key, val) {
// 递归监听对象的子属性
if (typeof val === 'object') {
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify();
console.log('执行依赖');
},
get() {
dep.dependSub();
return val;
}
});
}
后面侦测对象时只需要new 一下即可
let obj = {}
new Observer(obj);
最后可以进行测试,当数据变化时是否能够触发回调函数。
let vm = {
data: {
addr: 'hubeiwuahn'
},
$watcher: function (expOrFn, cb) {
new Watcher(this, "data." + expOrFn, cb);
}
}
new Observer(vm.data);
vm.$watcher('addr', function (newValue, oldValue) {
console.log('监听开始', newValue, oldValue);
});
vm.data.addr = 'beijing';