欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

简单了解 vue2.0 是如何用 Object.defineProperty() 来实现 双向绑定的

程序员文章站 2022-03-06 18:57:28
...
首先简单了解一下 Object.defineProperty():
  • defineProperty() 通过数据劫持 -> 给对象进行扩展 -> 属性进行设置
    • 数据劫持:把一个对象里的属性进行可配置(可写/可枚举/可删除), 然后再通过 set 和 get 对存取值进行逻辑上的扩展
  • defineProperty(obj, prop, descriptor) 中有三个参数(需要被定义属性的对象, 要定义或处理的属性的名称, 描述符(对属性的描述))
  • 是 Object 下的一个方法, 只能直接处理对象 不可以直接处理数组 和 函数

下面来看看是如何实现对 data 的监听
  • 简单监听 data 变化
	function defineProperty(target, key, value) {
		// 每一个属性定义的时候, 都会有 getter 和 setter
		Object.defineProperty(target, key, {
			get () {
				return value
			},
			set (newVal) {
				// 在这里面更新值, 且触发视图更新(调用视图更新的方法)
				console.log('视图更新')
				value = newVal
			}
		})
	}	

	// 监听方法
	function observe(target) {
		// 判断不是对象或数组这返回自身
		if (typeof target !== 'object' || target === null) {
			return target
		}
		
		// 重新定义各个属性
		for(let key in target) {
			defineProperty(target, key, target[key])
		}
	}

	// 视图更新方法
	function updateView() {
		console.log('视图更新')
	}
	
	// 定义数据
	const data = {
		name: '张三',
		age: 20
	}
	
	observe(data)
	data.name = '李四'
	console.log(data)

  • 深度监听 data 变化(当对象层级更深时, 需要进行深度监听)
	function defineProperty(target, key, value) {
		
		// 深度监听(递归)
		observe(value)
		
		// 核心 API
		Object.defineProperty(target, key, {
			get() {
				return value
			},
			set(newVal) {
				// 这只新值
				if (newVal !== value) {
					console.log('更新')
					
					// 深度监听 (如果将属性值设置为一个对象, 不再次进行深度监听, 会监听不到更新操作)
					// 例如: 在下面代码中的  data.info.address.home = '上海' 修改, 如果不在这里进行深度监听, 会监听不到这个操作
					observe(newVal)
					
					// 赋值
					value = newVal
	
					// 触发视图更新
					updateView() 
				}
			}
		})
	}
	
	
	function observe(target) {
		// 判断不是对象或数组这返回自身
		if (typeof target !== 'object' || target === null) {
			return target
		}
		
		// 重新定义各个属性
		for(let key in target) {
			defineProperty(target, key, target[key])
		}
	}
	
	
	// 视图更新方法
	function updateView() {
		console.log('视图更新')
	}
	
	// 定义数据
	const data = {
		name: '张三',
		age: 20,
		info: {
			address: '北京'
		}
	}
	
	// 监听数据
	observe(data)
	
	data.info.address = '深圳';
	data.info.address = {home: '南京'}
	data.info.address.home = '上海'
	data.sex = 'woman'; // 新增属性时, 无法监听到
	delete data.age; // 删除属性时, 无法监听到
	console.log(data);
	

  • 监听数组变化
	// 重新定义自己的数组原型(目的: 为了防止污染全局的 Array 原型)
	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);
			// 上面语法类似于 Array.prototype.方法名(push/pop 等).call(this, ...arguments)
		}
	})
	
	
	function defineProperty(target, key, value) {
		
		// 深度监听(递归)
		observe(value);
		
		// 核心 API
		Object.defineProperty(target, key, {
			get() {
				return value;
			},
			set(newVal) {
				// 这是新值
				if (newVal !== value) {
					console.log('更新');
					// 深度监听
					observe(newVal);
					
					// 赋值
					value = newVal;
					
					// 触发视图更新
					updateView();
				}
			}
		})
	}
	
	
	function observe(target) {
		// 判断不是对象或数组这返回自身
		if (typeof target !== 'object' || target === null) {
			return target;
		}
		
		
		// 判断是数组的话, 则把 target 的原型设置 成 arrProto
		if(Array.isArray(target)) {
			target.__proto__ = arrProto;
		}
		
		
		// 重新定义各个属性
		for(let key in target) {
			defineProperty(target, key, target[key]);
		}
	}
	
	
	// 视图更新方法
	function updateView() {
		console.log('视图更新');
	}
	
	
	// 定义数据
	const data = {
		name: '张三',
		age: 20,
		info: {
			address: '北京'
		},
		num: [10, 20, 30]
	}
	
	
	observe(data)
	data.num.push(40);
	console.log(data)

在这里稍微总结一下 Object.defineProperty() 的缺点:
  • 深度监听, 需要递归到底, 一次性计算量大
  • 无法监听新增和删除属性, 所以需要借助 vue.set 和 vue.delete 这两个 API 来设置
  • 无法原生监听数组, 需要特殊处理