关注微信公众号[码不停息]解锁更多优质文章😊
Object.defineProperty简单了解
- 定义
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
一脸懵逼中...
- 语法
Object.defineProperty(obj, prop, descriptor)
obj 要定义属性的对象。
prop 要定义或修改的属性的名称
descriptor 要定义或修改的属性描述符
obj
和prop
很好理解 比如我们定义一个变量为
const o = {
name:'公众号码不停息'
}
其中obj
指的就是o
,prop
指的就是o.name
下面我们主要看看descriptor
(比较懒直接截图啦😃)
直接看代码吧
let obj = {}
Object.defineProperty(obj, 'name', {
configurable: true, // 可删除
enumerable: true, //可枚举
writable: true, //可修改
value: '码不停息'
})
需要注意的是:value,writable 和get,set不能同时进行配置
上面的例子还可以写成
let obj = {}
let name = '码不停息'
Object.defineProperty(obj, 'name', {
configurable: true, // 可删除
enumerable: true, //可枚举
get() {
return name
},
set(newVal) {
name = newVal
}
})
obj.name // 码不停息
Object.defineProperty监听对象变化
现在我们有这样一个对象
let obj = {
name:'码不停息',
age:18,
love:['吃饭','睡觉','打豆豆']
}
我们想实现只要是对象里面的任何数据改变,都要跟新视图或者执行我们自己的逻辑,那我们需要怎么办呢?
我们似乎需要解决以下几个问题:
obj
对象有多个属性,可能需要__循环__添加到Object.defineProperty
里面obj
的属性也可能是对象或者数组,可能需要__递归__- 用户可能给
obj
赋值新的属性,这种情况可能需要 单独处理
那下面我们就试探性的去解决上面的问题
首先我们先做个实现响应式的函数 defineProperty
function defineProperty(obj, key, val){
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 读取方法
console.log('读取', key, '成功')
return val
},
set(newval) {
// 赋值监听方法
if (newval === val) return
observer(newval)
console.log('监听赋值成功', newval)
val = newval
// 可以执行渲染操作
}
})
}
其次我们再做个遍历函数observer
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
for (const key in obj) {
// 给对象中的每一个方法都设置响应式
defineProperty(obj, key, obj[key])
}
}
最后的代码结构如下
下面我们测试下看看我们更改数据时能不能做到响应式
可以看到我们的监听函数已经监听到数据的变化了,下面我们把数据弄复杂点
可以出现在我们虽然更改了
obj.haha.name
的值,但是并没有监听到数据的改变,这是为什么呢?
还记得我们上面说的 obj的属性也可能是对象或者数组,可能需要递归 下面我们来递归一下
这里递归实现非常简单,只需要把`observer`函数在`defineProperty` 重新调用一遍即可,在此判断传过来的`val`是不是一个对象,如果是一个对象在遍历下这个对象进行响应式收集
为了防止用户传进来的值也是一个对象,如用户可能这样使用
obj.name = {
name:xxx
}
我们在set方法里面也调用下observer
现在我们的响应式是否健壮了许多,下面我们来处理下 用户可能给obj赋值新的属性 的情况,如下所示,用户可能这样给obj
赋值
obj.mama = 'xxx'
// 注意 obj开始时并没有 mama
我们可以粗略的处理下这种情况,就是写个set
函数,用户使用set
赋新属性,函数里面再调用defineProperty
进行依赖收集,如下所示:
function set(obj, key, val) {
defineProperty(obj, key, val)
}
下面我们来测试下
可以看出通过
set
定义的新属性也具备响应式了,对此我们对对象的监听已经基本完成
Object.defineProperty实现对数组的监听
Object.defineProperty
是对象的方法监听不到数组的变更的,如下图所示:
那我们怎么来实现对数组的监听呢?答案就是重写
Array
的原型方法
const orginalProto = Array.prototype;
const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(method => {
arrayProto[method] = function () {
// 执行原始操作
orginalProto[method].apply(this, arguments)
console.log('监听赋值成功', method)
}
})
原理就是重写数组的七个原始方法,当使用者执行这些方法时,我们就可以监听到数据的变化,然后做些跟新操作,下面我们在observer
中加上关于对数组的判断
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
if (Array.isArray(obj)) {
// 如果是数组, 重写原型
obj.__proto__ = arrayProto
// 传入的数据可能是多维度的,也需要执行响应式
for (let i = 0; i < obj.length; i++) {
observer(obj[i])
}
} else {
for (const key in obj) {
// 给对象中的每一个方法都设置响应式
defineProperty(obj, key, obj[key])
}
}
}
其实我们发现 observer
其实是个递归,最后会把所有的数据都变成响应式,下面我们来测试下
完美!
总结
可以感觉到,在用Object.defineProperty
实现数据响应式时我们必须要遍历所有的数据,还需要重写数组的方法,性能消耗也比较大,我们知道Vue2.x
就是基于Object.defineProperty
实现数据响应式的但新版本的Vue3
放弃了Object.defineProperty
采用Proxy
重写了响应式系统,那Vue3
为什么要选择Proxy
?Proxy
又是如何实现数据拦截的呢?,我们下期分享用Proxy
如何实现响应式。