当前位置:网站首页>Object.defineProperty也能监听数组变化?

Object.defineProperty也能监听数组变化?

2022-06-25 07:44:00 InfoQ

本文简介

点赞 + 关注 + 收藏 = 学会了



首先,解答一下标题:
Object.defineProperty
 不能监听原生数组的变化。如需监听数组,要将数组转成对象。


在 
Vue2
 时是使用了 
Object.defineProperty
 监听数据变化,但我查了下 
文档
,发现 
Object.defineProperty
 是用来监听对象指定属性的变化。没有看到可以监听个数组变化的。

但 
Vue2
 有的确能监听到数组某些方法改变了数组的值。本文的目标就是解开这个结。


基础用法

Object.defineProperty() 文档

关于 
Object.defineProperty()
 的用法,可以看官方文档。

基础部分本文只做简单的讲解。

语法

Object.defineProperty(obj, prop, descriptor)

参数

  • obj
     要定义属性的对象。
  • prop
     要定义或修改的属性的名称或 
    Symbol
     。
  • descriptor
     要定义或修改的属性描述符。

const data = {}
let name = '雷猴'

Object.defineProperty(data, 'name', {
 get() {
 console.log('get')
 return name
 },
 set(newVal) {
 console.log('set')
 name = newVal
 }
})

console.log(data.name)
data.name = '鲨鱼辣椒'

console.log(data.name)
console.log(name)

上面的代码会输出

get
雷猴
set
鲨鱼辣椒
鲨鱼辣椒


上面的意思是,如果你需要访问 
data.name
 ,那就返回 
name
 的值。

如果你想设置 data.name ,那就会将你传进来的值放到变量 
name
 里。

此时再访问 
data.name
 或者 
name
 ,都会返回新赋予的值。


还有另一个基础用法:
“冻结”指定属性

const data = {}

Object.defineProperty(data, 'name', {
 value: '雷猴',
 writable: false
})

data.name = '鲨鱼辣椒'
delete data.name
console.log(data.name)

这个例子,把 
data.name
 冻结住了,不管你要修改还是要删除都不生效了,一旦访问 
data.name
 都一律返回 
雷猴
 。

以上就是 
Object.defineProperty
 的基础用法。


深度监听

上面的例子是监听基础的对象。但如果对象里还包含对象,这种情况就可以使用递归的方式。

递归需要创建一个方法,然后判断是否需要重复调用自身。

// 触发更新视图
function updateView() {
 console.log('视图更新')
}

// 重新定义属性,监听起来(核心)
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
 }

 // 重新定义各个属性(for in 也可以遍历数组)
 for (let key in target) {
 defineReactive(target, key, target[key])
 }
}

// 准备数据
const data = {
 name: '雷猴'
}

// 开始监听
observer(data)

// 测试1
data.name = {
 lastName: '鲨鱼辣椒'
}

// 测试2
data.name.lastName = '蟑螂恶霸'

上面这个例子会输出2次“视图更新”。



我创建了一个 
updateView
 方法,该方法模拟更新 
DOM
 (类似 
Vue
的操作),但我这里简化成只是输出 “视图更新” 。因为这不是本文的重点。


测试1
 会触发一次 “视图更新” ;
测试2
 也会触发一次。

因为在 
Object.defineProperty
 的 
set
 里面我有调用了一次 
observer(newValue)
 , 
observer
 会判断传入的值是不是对象,如果是对象就再次调用 
defineReactive
 方法。

这样可以模拟一个递归的状态。


以上就是 
深度监听
 的原理,其实就是递归。

但递归有个不好的地方,就是如果对象层次很深,需要计算的量就很大,因为需要一次计算到底。


监听数组

数组没有 
key
 ,只有 
下标
。所以如果需要监听数组的内容变化,就需要将数组转换成对象,并且还要模拟数组的方法。

大概的思路和编码流程顺序如下:

  • 判断要监听的数据是否为数组
  • 是数组的情况,就将数组模拟成一个对象
  • 将数组的方法名绑定到新创建的对象中
  • 将对应数组原型的方法赋给自定义方法



代码如下所示

// 触发更新视图
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 data = {
 nums: [10, 20, 30]
}

// 监听数据
observer(data)

data.nums.push(4) // 监听数组

上面的代码之所以没有直接修改数组的方法,如

 Array.prototype.push = function() {
 updateView()
 ...
 }

因为这样会污染原生 
Array
 的原型方法,这样做会得不偿失。

以上就是使用 
Object.defineProperty
 的方法。

如需监听更多方法,可以在数组 
['push', 'pop', 'shift', 'unshift', 'splice']
 中添加。


综合代码

// 深度监听
function updateView() {
 console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原形指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
// arrProto.push = function () {}
// arrProto.pop = function() {}
['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 不具备监听数组的能力
 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])
 }
}

总结

上面的代码主要是模拟 
Vue 2
 监听数据变化,虽然好用,但也有缺点。

缺点

  • 深度监听,需要递归到底,一次计算量大
  • 无法监听新增属性/删除属性(所以需要使用 Vue.set 和 Vue.delete)
  • 无法原生监听数组,需要特殊处理


所以在 
Vue 3
 中,把 
Object.defineProperty
 改成 
Proxy
 。

但 
Proxy
 的缺点也很明显,就是兼容性问题。所以需要根据你的项目来选择用 
Vue 2
 还是 
Vue 3
 。

原网站

版权声明
本文为[InfoQ]所创,转载请带上原文链接,感谢
https://xie.infoq.cn/article/460d66ada7f39aeb7a4bb3fdc