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

首先简单了解一下 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 来设置
  • 无法原生监听数组, 需要特殊处理
上一篇:由(a === 1 && a === 2 && a === 3) === true 引发的一系列思考


下一篇:js------Object.defineProperty设置或修改对象中的属性