Vue.js原理之vue响应式/怎样监听data变化

  1. 组件data的数据一旦发生变化,立刻触发视图更新。
  2. Vue.js实现响应式的关键 ⇒ 核心API ⇒
    Object.defineProperty(3.0前,Vue.js3.0启用的事Proxy,但是Proxy有兼容性问题)。
  3. Object.defineProperty()缺点(这些缺点只是几个知识点而已,并不是在放大它们。下方示例中会有说明):
  • (1)深度监听需要递归到底,一次性计算量大。
  • (2)无法监听新增/删除属性(Vue.js2.0中对象属性的新增/删除一定要用Vue.set/Vue.delete)。
  • (3)无法原生监听数组,需要特殊处理。

示例一:展示最基本的数据响应式

<script type="text/javascript">
			//触发更新视图
			function updateView(){
				console.log('视图更新')
			}
			
			//重新定义属性,监听起来
			function defineReactive(target, key, value){
				//核心API
				Object.defineProperty(target, key, {
					get(){
						return value;
					},
					set(newValue){
						if(newValue !== value){
							//设置新值
							//注意,value一直在闭包中,此处设置完之后,再get时也是
							value = newValue
							
							//触发更新视图
							updateView();
						}
					}
				})
			}
			
			
			//监听对象属性
			function observer(target){
				if(typeof target !== 'object' || target === null){
					//不是对象或数组
					return target;
				}
				
				//重新定义各个属性(for  in  也可以遍历数组)
				for(let key  in target){
					defineReactive(target, key, target[key])
				}
			}
			
			//准备数据
			const objA= {
				name:'zhangsan',
				age:20,
//				info:{
//					address:'北京',
//				},
//				nums:[10, 20, 30]
			}
			
			//监听数据
			observer(objA);
			
			//测试
			data.name = 'list';
			data.age = 21;
			//data.x = '100';//新增属性,监听不到,所以有vue.set
			//delete data.name;//删除属性,监听不到,所以有vue.delete
			//data.info.address = '上海';//深度监听
			//data.nums.push(4)//监听数组
		</script>

这个示例会在控制台输出两次’视图更新’,
在observer函数中监听对象objA的变化,遍历对象,监听每个属性的变化,因为测试时给objA的name属性个age属性都重新赋值,所以都会触发updateView()更新视图,因此控制台会输出两次’视图更新’。

示例二:对象深度监听
还是示例一中的代码,这时我们把对象objA的info属性解开注释

const objA = {
				name:'zhangsan',
				age:20,
				info:{
					address:'北京',
				},
//				nums:[10, 20, 30]
			}

     data.info.address = '上海';//深度监听

vue 监听eldatepicker变化导致批量调用api_vue.js

控制台依旧还是输出两次’视图更新’。这时的defineReactive函数监听不到info.address的变化,因为这个函数只做了单层级的监听,需要在defineReactive函数中再调用一次observer函数。

//重新定义属性,监听起来
			function defineReactive(target, key, value){
				observer(value)
				
				//核心API
				Object.defineProperty(target, key, {
					get(){
						return value;
					},
					set(newValue){
						if(newValue !== value){
							//设置新值
							//注意,value一直在闭包中,此处设置完之后,再get时也是
							value = newValue
							
							//触发更新视图
							updateView();
						}
					}
				})
			}

此时当oberver函数中传入info:{address:‘北京’}时,因为是对象类型所以不走if判断,又做了一遍forin循环。
此时会在控制台输出三次’视图更新’。

此时如果改变objA对象的age属性值,从数值型换成对象类型,会是什么结果呢。
objA.age = {num:21};
此时会在控制台输出四次’视图更新’。
然而当再次修改时,objA.age.num=22; 控制台依然是四次’视图更新’输出。监听并没有监测出来objA.age.num的变化。
要解决这个问题,必须在核心APIObject.defineProperty的set函数中再调用一次oberserve监听对象函数,一次递归到底。

function defineReactive(target, key, value){
				observer(value);//深度监听
				
				//核心API
				Object.defineProperty(target, key, {
					get(){
						return value;
					},
					set(newValue){
						if(newValue !== value){
							observer(newValue);//深度监听
							
							//设置新值
							//注意,value一直在闭包中,此处设置完之后,再get时也是
							value = newValue
							
							//触发更新视图
							updateView();
						}
					}
				})
			}

这时候控制台会输出五次’视图更新’。

示例三:js原生方法新增/删除属性
如果给对象新增一个属性,或删除一个属性,这个时候是Object.defineProperty()是监听不到的。
objA.x = ‘100’;//新增属性,监听不到,所以有vue.set
delete objA.name;//删除属性,监听不到,所以有vue.delete

示例四:监听数组的变化
解开对象objA中的num属性

const objA = {
				name:'zhangsan',
				age:20,
				info:{
					address:'北京',
				},
				nums:[10, 20, 30]
			}

测试部分只留objA.nums.push(4)//监听数组,其它都注释掉。
此时代码执行虽然能进入observer函数进行遍历,但是Object.defineProperty()不具备监听数组的能力。
此时需要在代码上面重新定义数组的原型,为防止污染数组原有的原型,所以要重新定义数组原型。见下方代码

//重新定义数组原型
			const oldArrayProperty = Array.prototype;
			//创建新对象,原型指向oldArrayProperty,再扩展新的方法不会影响原型
			const arrProto = Object.create(oldArrayProperty);
			['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
				arrProto[methodName] = function(){
					updateView();//触发更新视图
					oldArrayProperty[methodName].call(this, ...arguments);
				}
			})

还需要把observer函数做一个判断类型是否是数组的分支

//监听对象属性
			function observer(target){
				if(typeof target !== 'object' || target === null){
					//不是对象或数组
					return target;
				}
				
				//类型是数组走这个分支
				if(Array.isArray(target)){
					target.__proto__ = arrProto;
				}
				
				//重新定义各个属性(for  in  也可以遍历数组)
				for(let key  in target){
					defineReactive(target, key, target[key])
				}
			}

下方是完整代码

<script type="text/javascript">
			//触发更新视图
			function updateView(){
				console.log('视图更新')
			}
			
			//重新定义数组原型
			const oldArrayProperty = Array.prototype;
			//创建新对象,原型指向oldArrayProperty,再扩展新的方法不会影响原型
			const arrProto = Object.create(oldArrayProperty);
			['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
				arrProto[methodName] = function(){
					updateView();//触发更新视图
					oldArrayProperty[methodName].call(this, ...arguments);
				}
			})
			
			
			//重新定义属性,监听起来
			function defineReactive(target, key, value){
				observer(value);//深度监听
				
				//核心API
				Object.defineProperty(target, key, {
					get(){
						return value;
					},
					set(newValue){
						if(newValue !== value){
							observer(newValue);//深度监听
							
							//设置新值
							//注意,value一直在闭包中,此处设置完之后,再get时也是
							value = newValue
							
							//触发更新视图
							updateView();
						}
					}
				})
			}
			
			
			//监听对象属性
			function observer(target){
				if(typeof target !== 'object' || target === null){
					//不是对象或数组
					return target;
				}
				
				//类型是数组走这个分支
				if(Array.isArray(target)){
					target.__proto__ = arrProto;
				}
				
				//重新定义各个属性(for  in  也可以遍历数组)
				for(let key  in target){
					defineReactive(target, key, target[key])
				}
			}
			
			//准备数据
			const objA = {
				name:'zhangsan',
				age:20,
				info:{
					address:'北京',
				},
				nums:[10, 20, 30]
			}
			
			//监听数据
			observer(objA);
			
			
			//测试
//			objA.name = 'list';
//			objA.age = 21;
			//objA.x = '100';//新增属性,监听不到,所以有vue.set
			//delete objA.name;//删除属性,监听不到,所以有vue.delete
//			objA.info.address = '上海';//深度监听
			objA.nums.push(4)//监听数组
//			objA.age = {num:21};
//			objA.age.num=22;	

			
		</script>