<深入浅出Vuejs>变化侦测_Object

本文深入探讨Vue.js的响应式系统,包括通过defineReactive实现数据侦测,Watcher类管理依赖,以及Dep类收集和通知依赖更新。Vue使用getter/setter以及Observer类递归转换数据为响应式,确保视图与数据同步。
摘要由CSDN通过智能技术生成

        Vue通过template模板来建立状态数据和dom之间的关系,将数据绑定在模板上,而模板又用于渲染生成dom。故,当状态数据发生变化时,可以通过更新模板来实现dom的自动更新。这中间存在三个过程:

  1. 侦测数据(对状态数据进行监测,以便当数据发生变化时,我们能知道数据变化了)
  2. 通知依赖(我们知道数据变化了以后,是不是要通知依赖了我们这个数据的东西去进行更新啊,其实这里应该还有一步,就是收集依赖,因为只有收集了依赖,我们才能知道我们需要通知哪些东西)
  3. 更新数据(依赖者收到通知后要进行对象的dom更新,这部分以后再说

变化侦测有两种类型:Push 和 Pull.

        Vuejs属于Push,即当状态发生改变时,Vuejs 立刻就知道了,而且在一定程度上知道哪些状态发生了变化,因此Vue可以进行更细粒度的更新.Vue引入虚拟DOM,状态绑定的不再是具体DOM,而是一个组件.降低依赖追踪是所消耗的内存.

        JS有两种变化侦测手段:

  1. Object.defineProperty()  -VUE2
  2. Proxy:  Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。-VUE3

这里我们主要介绍VUE2的方法:

1.defineReactive():用来将非响应式数据转换成响应式数据(被侦测的数据).

function defineReactive(data, key, val) {
	Object.defineProperty(data, key, function() {
		enumerable: true, // 该属性是否可被枚举
		configurable: true, // 该属性的配置后续是否可进行更改
		get: function () { // 用于获取属性值
			return val
		},
		set: function (newVal) { // 设置属性值
			if (newVal === val) return
			// 用新值替换旧值
			val = newVal
		}
	})
}
// 参数解释:
// data 表示定义属性的对象,key表示对象的属性,val是 要定义或修改的属性描述符

// 假如vue下有个这样的数据  cat: {name: kitty}
// 我们需要劫持cat下的name属性
// 那么只需要 defineReactive(cat, 'name', kitty) 就完成了对cat.name的数据侦测
// 访问cat.name,便会触发get函数
// 通过cat.name = 'bob'时, 就会触发set函数

如此一来 ,通过set和get函数,我们可以随时知道数据的变化了,然后通知依赖进行更新.

2.Watcher类:用于统一管理依赖.在VUE template模板中dom节点所依赖

export default class Watcher {
	/*
	 * vm 是vue实例
	 * expr 是属性的key。如 data.a.b.c
	 * cb 是回调函数
	 */
	 
	constructor (vm, expr, cb) {
		this.vm = vm
		this.value = this.get() // new 实例时,先存储一下oldVal
	     //getPathData用于解析属性路径
	     //data.a.b.c data 对象下的a对象下的b对象下的c属性
		this.getter = getPathData(expr)
		
		get () {
            window.target = this
			let value = this.getter(this.vm)
            window.target = undefined
            return value
		}
		
		update () {
			const oldValue = this.value // 定义一个变量存储旧的值
			this.value= this.getValue() // 重新获取新的值
			this.cb(newVal, oldVal) // 触发回调函数,然后在回调函数中就可以做具体的更新了
		}
	}
}
// 这个函数主要用于解析路径,并返回一个获取属性值的函数,利用了闭包
export function getPathData(expr) {
    //利用正则表达式判断是不是合法的路径
	const reg = /[^\w.$/
	if (!reg.test(expr)) return
	//将data.a.b.c, 分解成['data', 'a', 'b', 'c']
	const arr = expr.split('.')
	return function (vm) {
		if (!vm) return
		let obj = vm // vm是vue实例
		for (let i = 0; i < arr.length; i++) {
			// 第一次循环,obj = vm.data  相当于vm就拿到了vue下的data对象
			// 第二次循环 obj = obj['a'] 相当于就是vm.data.a的值
			// 第三次循环 obj = obj['b'] 相当于vm.data.a.b的值
			// 第四次循环 obj = obj['c'] 是不是就拿到了vm.data.a.b.c的值啊
			obj = obj[arr[i]]
		}
		return obj // 返回获取到的值
	}
}

Watcher类中,get方法把window.target设置成this,也就是当前的watcher实例,然后通过getter读取data.a.b.c的值,会相应的触发对应响应式数据属性的get()函数,该函数利用window.target获取依赖,并做相应的处理(详见下一部分) .当data.a.b.c发生变化时,会对依赖列表里所有的依赖循环执行update方法,通过回调函数来执行具体的更新操作.

3.收集依赖:每个数据都可能有多个依赖者,我们给每个数据都添加一个依赖的管理者Dep.这个类可以控制一个属性所对应的依赖的添加和删除,以及向依赖发送通知.

export default class Dep () {
	constructor() {
		this.sub = [] // 依赖者数组,用来存放依赖者watcher实例
	}

	addSub (sub) { // 添加依赖
		this.sub.push(sub)
	}

	remove (sub) { // 删除依赖
		remove(this.sub, sub)
	}

	// 通知方法,用于通知各个依赖者进行数据更新
	notify () {
		const arr = this.subs.slice() // 先拷贝一份
		for (let i = 0; i < arr.length; i++) {
			arr[i].update() // 让每一个watcher去调用自己的更新函数
		}
	}
}

export function remove (subs, sub) {
	if (subs.length && subs.length > 0) {
		const index = subs.indexOf(sub)
		if (index > -1) {
			return subs.splice(index, 1)
		}
	}
}
function defineReactive(data, key, val) {
	let dep = new Dep()
	Object.defineProperty(data, key, function() {
		enumerable: true, // 该属性是否可被枚举
		configurable: true, // 该属性的配置后续是否可进行更改
		get: function () { // 用于获取属性值
			// 收集依赖
			if (window.target) {
				dep.addSub(window.target)
			}
			return val
		},
		set: function (newVal) { // 设置属性值
			if (newVal === val) return
			// 用新值替换旧值
			val = newVal
			// 数据发生变化时,通知依赖者
			dep.notify()
		}
	})
}

 对defineReactive函数做出一些更改,在访问数据触发get方法时,获取window.target(当前watcher实例) ,将其加入Dep中进行管理.当修改数据触发set方法时,向所有和其相关的数据发送通知.

        以上就是 侦测数据,收集依赖,向依赖发送通知进行更新的整条流程,但只能侦测data中的某一个属性,而我们需要将所有的属性(及其子属性)都侦测到,因此我们需要封装一个类用于将data中的所有属性都转换成响应式数据.即Observer.

export default class Observer {
	// 参数就是需要被侦测的数据。首次进来参数肯定是vue的data对象
	constructor (data) {
		this.value = data

		if (Array.isArray(this.value)) {
			// 侦测数组,详见深入浅出VUEjs_变化侦测_Array
		} else {
			// 如果不是数组,必然就是对象
			this.walk(this.value) // vue内部用来递归转换响应式数据
		}
	}

	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 Obsever(val)
	}
	let dep = new Dep()
	Object.defineProperty(data, key, function() {
		enumerable: true, // 该属性是否可被枚举
		configurable: true, // 该属性的配置后续是否可进行更改
		get: function () { // 用于获取属性值
			// 收集依赖
			if (window.target) {
				dep.addSub(window.target)
			}
			return val
		},
		set: function (newVal) { // 设置属性值
			if (newVal === val) return
			// 用新值替换旧值
			val = newVal
			// 数据发生变化时,通知依赖者
			dep.notify()
		}
	})
}

        定义好了Observer,要在defineReactive中新增一条判断,如果当前属性值还是object,那么我们继续new Observer(val)去递归进行侦测。

        在每次new Vue()实例的时候都在其构造器里new Observer(this.$data)将其数据转换为响应式数据. 

        关于以上各模块关系的一个理解:

PS 关于Object的一些问题:

        前面介绍了,对于Object的侦测是通过getter/setter来追踪的,而这是这一点,导致vue2无法识别一些数据变化.

        当我们为对象添加或者是删除一些属性时,vue2无法侦测这个变化,因为这些操作不触发getter和setter.而ES6之前,js并没有提供元编程能力(ES6 可以通过Proxy解决),所以vue2提供了$set和$delete两个API.

        本文章仅为本人学习总结,如果有不足还请各位指出!

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值