Vuejs对象(Object)的变化侦测

本文深入探讨Vue.js中变化侦测的原理,包括推模式的特点、使用Object.defineProperty进行属性追踪的方法、依赖收集的过程及Watcher的作用。此外,还介绍了如何通过递归侦测所有属性来完善变化侦测系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

何为变化侦测:
变换侦测的类型:

  • “推” push
  • “拉” pull
    Vue.js的变化侦测属于"推"。当状态变化的时候vue就马上会知道。变化侦测也是vue响应式系统的核心。由于vue使用的是push类型,所以每个状态的依赖就会越多,所以需要的内存开销越大,因此vue引入了虚拟DOM,即一个状态所绑定的依赖不在是具体的DOM节点而是一个组件,状态发生改变就会通知到组件,由组件内部使用虚拟DOM进行比对。

如何追踪变化:
两种方式Object.definePropertyProxy。因为ES6浏览器支持问题目前vueJS还是用的是Object.defineProperty
追踪实现:

// obj: 要定义属性的对象,这里就是要监听的对象
// prop: 要配置或修改属性的名称或Symbol
// descriptor: 要定义或修改的属性描述
// enumerable: 默认为false,为 true 时该属性才会在对象的枚举属性中
// configurable:默认为false,为 true 时该属性的描述才能被改变,
// 同时该属性也可以从对应的对象上删除
function defindeReactive (obj, prop, descriptor) {
	Object.defineProperty(obj, prop, {
		enumerable: true, // 是否可枚举
		configurable: true, // 是否可配置
		get: function () {
			return descriptor;
		},
		set: function (newVal) {
			if(descriptor === newVal) {
				return;
			}
			descriptor = newVal;
		}
	});
}

每当从obj的prop中读取数据时,get函数被触发,每当往obj的prop中设置数据的时候,set函数会被触发。

如何收集依赖:
为什么要收集依赖?
因为当数据的属性发生变化时,可以根据依赖通知那些使用了该数据的位置,即时更新数据,根据上述的定义的方法,可以在getter中收集依赖,在setter中触发依赖。

收集的依赖存放到哪里:
我们可以为把prop放到一个数组中进行统一管理。如果这个依赖是一个函数,保存在window.rely上。可以把上述方法修改一下

function defindeReactive (obj, prop, descriptor) {
	let dep = [];
	Object.defineProperty(obj, prop, {
		enumerable: true,
		configurable: true,
		get: function () {
			dep.push(window.rely);
			return descriptor;
		},
		set: function (newVal) {
			if(descriptor === newVal) {
				return;
			}
			for (let i = 0; i < dep.length; i++) {
				dep[i](newVal, descriptor);
			}
			descriptor = newVal;
		}
	});
}

这里的dep数组,就可以把所有的依赖收集起来,在setter函数被触发时就可以循环遍历所有的依赖。为降低代码的耦合度,可以封装一个Dep类,为我们专门收集和管理依赖

export default class Dep {
	constructor () {
		this.subs = [];
	}
	addSub (sub) {
		this.subs.push(sub);
	}
	removeSub () {
		remove(this.subs, sub);
	}
	depend () {
		if(window.rely) {
			this.addSub(window.rely);
		}
	}
	notify () {
		const subs = this.subs.slice();
		for (let i = 0; i < subs.length; i++) {
			subs[i].update();
		}
	}
	function remove (arr, item) {
		if (arr.length) {
			const index = arr.indexOf(item);
			if(index > -1) {
				return arr.splice(index, 1);
			}
		}
	}
}

之后再次修改defineReactive,将参数语义化

function defindeReactive (data, key, val) {
	let dep = new Dep();
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get: function () {
			dep.depend();
			return val;
		},
		set: function (newVal) {
			if(val === newVal) {
				return;
			}
			val = newVal;
			dep.notify();
		}
	});
}

属性变化之后通知谁:
要通知用到数据的地方,使用这个数据的地方很多、而且类型可能不同,既有可能是模板也能是用户自己写的一个watch。这时就需要一个可以处理这些情况的类。然后可以在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。然后由它通知其他地方。这就是Watcher。可以根据Watcher侦听属性变化,再由Watcher发出通知。

Watcher:
可以把Watcher看成一个中介的角色,数据发生变化的时候会通知它,它会通知其他地方。当属性变化时,会触发第二个参数中的函数。
怎么实现?
watcher实例添加到数据属性的Dep中,当数据属性值发生变化的时候,通知Watcher。然后Watcher执行参数中的回调函数

export default class Wathcer {
	constructor (vm, expOrFn, callBack) {
		this.vm = vm;
		this.getter = parsePath(expOrFn);
		this.callBack = callBack;
		this.value = this.get();
	}
	get () {
		window.rely = this;
		let value = this.getter.call(this.vm, this.vm);
		wndow.rely = undefined;
		return value;
	}
	update () {
		const oldValue = this.value;
		this.value = this.get();
		this.callBack.call(this.vm, this.value, oldValue);
	}
}

这样就可以把Watcher的实例添加到数据的属性中。
因为在get方法中先把window.rely设置为this,当前Watcher实例,这样就可以在读取数据属性值的时候触发getter函数。触发getter,就会开始收集依赖,从Window.rely中读取依赖并添加到Dep中,只要先在window.rely赋一个this,然后再去读这个值,去触发getter,就可以把this主动添加到keypathDep中,当依赖注入到Dep中,每当数据属性的值发生变化的时候,就会触发update方法就会执行回调函数,将valueoldValue传入到参数中。这样不管是vm.$watch('add', (value, oldValue) => {}),还是在data定义的数据属性,都可以通过Watcher来通知自己是否需要发生变化。

那么parsePath是如何读取一个字符串的keypath的?

const bailRE = /[^\w.$]/;
export function parsePath (path) {
	if (bailRE.test(path)) {
		return;
	}
	const segments = path.split('.');
	return function (obj) {
		for (let i = 0; i < segments.length; i++) {
			if(!obj) {
				return;
			}
			obj = obj[segments[i]];
		}
		return obj;
	}
}

先将keypath分割成数组,然后循环数组一层一层去读数据,最后拿到就是keypath中想要的数据。

递归侦测所有的key:
目前已经基本完成变化侦测功能了,但目前代码只能侦测数据中的某一个属性,如果把数据中所有属性(包括子属性)都侦测到,岂不美哉!!!,这里需要封装一个类,将数据内的所有属性包括子属性都转换成getter/setter形式,然后去追踪其变化。

/*
Observer类会附加到每个被侦测的object上
一旦被附加上,Observer会将Object的所有属性转化为getter/setter的形式
来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*/
export class Observer {
	constructor (value) {
		this.value = value;
		if (!Array.isArray(value)) {
			this.walk(value);
		}
	}
/*
walk会将每个属性都转换成getter/setter形式侦测变化,只有数据类型为Object被调用
*/
walk (obj) {
	const keys = Object.keys(obj);
	for (let i = 0; i < keys.length; i++) {
		defineReactive(obj, keys[i], obj[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,
		get: function () {
			dep.depend();
			return val;
		},
		set: function (newVal) {
			if(val === newVal) {
				return;
			}
			val = newVal;
			dep.notify();
		}
	});
}

定义的Observer类可以将一个正常的object转换成被侦测的object。然后判断数据的类型、只有object类型的数据才会调用walk将每个属性转化成getter/setter的形式来侦测变化。最后在defineReactive中新增new Observer(val)来递归子属性,这样我们就可以把data中的所有属性包括子属性都转换为getter/setter的形式来侦测变化。当data中的属性发生变化的时候,就会通知相关依赖进行变化。

有关Object变换侦听的问题key:
经上述介绍数据的变化是通过getter/setter来追踪的,由于这种追踪方式导致一些语法中即便是数据发生了变化,Vue.js也追踪不到
例子:

let vm = new Vue({
	el: '#app',
	template: '#demo-template',
	methods: {
		action () {
			this.obj.name = 'berwin';
		}
	},
	data: {
		obj: {}
	}
});

action方法中,在obj中新增了name属性,Vue.js无法侦测到这一变化,所以不会向依赖发送通知。Vue.js通过Object.definProperty来将对象的key转换成getter/setter的形式来追踪变化,但是它们只能追踪到一个数据是否被修改,无法追踪新增或删除属性。在Es6之前JavaScript没有提供元编程的能力,无法侦测到对象的添加或删除属性。Vue.js提供了两个API vm.$setvm.$delete

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值