简单了解 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 来设置
- 无法原生监听数组, 需要特殊处理