数据劫持实现原理

二、针对 Object 类型的劫持

对于 Object 类型,主要劫持其属性的读取与设置操作。在 JavaScript 中对象的属性主要由一个字符串类型的“名称”以及一个“属性描述符”组成,属性描述符包括以下选项:

value: 该属性的值;

writable: 仅当值为 true 时表示该属性可以被改变;

get: getter (读取器);

set: setter (设置器);

configurable: 仅当值为 true 时,该属性可以被删除以及属性描述符可以被改变;

enumerable: 仅当值为 true 时,该属性可以被枚举。

上述 setter 和 getter 方法就是供开发者自定义属性的读取与设置操作,而设置对象属性的描述符则少不了 Object.defineProperty() 方法:

function defineReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { console.log(' === 收集依赖 === ') console.log(' 当前值为:' + val) return val }, set (newValue) { console.log(' === 通知变更 === ') console.log(' 当前值为:' + newValue) val = newValue } }) } const student = { name: 'xiaoming' } defineReactive(student, 'name') // 劫持 name 属性的读取和设置操作

上述代码通过 Object.defineProperty() 方法设置属性的 setter 与 getter 方法,从而达到劫持 student 对象中的 name 属性的读取和设置操作的目的。

读者可以发现,该方法每次只能设置一个属性,那么就需要遍历对象来完成其属性的配置:

Object.keys(student).forEach(key => defineReactive(student, key))

另外还必须是一个具体的属性,这也非常的致命。

假如后续需要扩展该对象,那么就必须手动为新属性设置 setter 和 getter 方法,**这就是为什么不在 data 中声明的属性无法自动拥有双向绑定效果的原因 **。(这时需要调用 Vue.set() 手动设置)

以上便是对象劫持的核心实现,但是还有以下重要的细节需要注意:

1、属性描述符 - configurable

在 JavaScript 中,对象通过字面量创建时,其属性描述符默认如下:

const foo = { name: '123' } Object.getOwnPropertyDescriptor(foo, 'name') // { value: '123', writable: true, enumerable: true, configurable: true }

前面也提到了 configurable 的值如果为 false,则无法再修改该属性的描述符,所以在设置 setter 和 getter 方法时,需要注意 configurable 选项的取值,否则在使用 Object.defineProperty() 方法时会抛出异常:

// 部分重复代码 这里就不再罗列了。 function defineReactive (obj, key) { // ... const desc = Object.getOwnPropertyDescriptor(obj, key) if (desc && desc.configurable === false) { return } // ... }

而在 JavaScript 中,导致 configurable 值为 false 的情况还是很多的:

可能该属性在此之前已经通过 Object.defineProperty() 方法设置了 configurable 的值;

通过 Object.seal() 方法设置该对象为密封对象,只能修改该属性的值并且不能删除该属性以及修改属性的描述符;

通过 Object.freeze() 方法冻结该对象,相比较 Object.seal() 方法,它更为严格之处体现在不允许修改属性的值。

2、默认 getter 和 setter 方法

另外,开发者可能已经为对象的属性设置了 getter 和 setter 方法,对于这种情况,Vue 当然不能破坏开发者定义的方法,所以 Vue 中还要保护默认的 getter 和 setter 方法:

// 部分重复代码 这里就不再罗列了 function defineReactive (obj, key) { let val = obj[key] //.... // 默认 getter setter const getter = desc && desc.get const setter = desc && desc.set Object.defineProperty(obj, key, { get () { const value = getter ? getter.call(obj) : val // 优先执行默认的 getter return value }, set (newValue) { const value = getter ? getter.call(obj) : val // 如果值相同则没必要更新 === 的坑点 NaN!!!! if (newValue === value || (value !== value && newValue !== newValue)) { return } if (getter && !setter) { // 用户未设置 setter return } if (setter) { // 调用默认的 setter 方法 setter.call(obj, newValue) } else { val = newValue } } }) }

3、递归属性值

最后一种比较重要的情况就是属性的值可能也是一个对象,那么在处理对象的属性时,需要递归处理其属性值:

function defineReactive (obj, key) { let val = obj[key] // ... // 递归处理其属性值 const childObj = observe(val) // ... }

递归循环引用对象很容易出现递归爆栈问题,对于这种情况,Vue 通过定义 ob 对象记录已经被设置过 getter 和 setter 方法的对象,从而避免递归爆栈的问题。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/ca0f430eb3339254e44ffbd2be9486e2.html