前言
我在学习Vue2时,学到数据响应式时,一直很疑惑底层具体到底怎么实现的,我知道是用到了Object.definedProperty(),但是也只是知道的迷迷糊糊的,所以我觉得很多刚刚接触Vue的小伙伴也一定有这样的疑惑,所以我决定写下这篇文章,一是让自己印象更深刻,二是希望对小伙伴们有点帮助,最后也希望看到这篇文章的小伙伴,觉得我说的不对的,可以帮我纠正。
Object.definedProperty()
Vue2的数据响应式原理一定离不开Object.definedProperty(),所以,我们先来看一下 Object.definedProperty() 到底是个什么东西。
其实从翻译角度看也可以看出来这是在为一个对象定义一个属性,具体怎么使用,我们来举个栗子
const data = {}
Object.defineProperty(data, 'name', {
value: 'RachelLenyan'
})
在这里,我们定义了一个对象,里面没有任何属性,然后我们使用Object.definedProperty()给data对象设置了一个name属性,值为value的后面的值,可以看到控制台可以打印出对应的属性值
那设置属性直接**data.name = ‘RachelLenya’**不行吗,干嘛费老大劲去整这出,这个Object.defineProperty肯定不止就设置个属性值,那我们再看看还可以这么配置,其实这个部分看MDN也很快也很简单的
const data = {
age: 19,
major: 'student'
}
Object.defineProperty(data, 'name', {
value: 'RachelLenyan',
enumerable: false
})
在这里我们只加了一个enumerable : false,就可以看到刚刚给data添加的属性无法被枚举,在这里我也不赘述了,直接一次性贴出来吧, 或者MDN上也说的很清楚
::: block-1
配置的属性
configurable 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false。
enumerable 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false。 数据描述符还具有以下可选键值:
value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined。
writable 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false。 ::: 当然这不是特别重点的地方,主要是我们可以实现对对象属性的数据劫持。
数据劫持
那什么是数据劫持呢,我们可以简单想象一下,你去超市买个个东西,本来要送回家的,结果半路被你的冤种闺蜜劫走了,本来要送回家的东西,送到了他的肚子里,这不就是劫持吗,那数据劫持不就是对数据进行抢劫吗。
说是说明白了,直接上代码瞅一下吧
const data = {
name: 'xxx'
}
Object.defineProperty(data, 'name', {
get () {
return '你的名字被我抢了'
}
})
无论我原先的name属性是什么,在我访问属性时,这个‘xxx’就快要给我了,结果被get(){} 方法劫持了,然后返回一个新的属性值
与之对应的还有一个 set(newVal){} 方法,当你修改对应的属性的属性值时,set(newVal){} 就会被调用,它会拿到你修改的那个属性值,然后内部代码怎么写就是看你自己了。
在这里是不是有一点思路了,我要是在set(){}里面把新拿到的值给这属性,然后在get(){} 里面返回这个属性的属性值不就好了吗,哇,原来响应式这么简单哇,于是我高高兴兴的试了一下
const data = {
name: 'xxx'
}
Object.defineProperty(data, 'name', {
get () {
return data.name
},
set (newVal) {
data.name = newVal
}
})
好家伙,这一片红看的是心烦意乱啊,这是怎么回事呢,来,我们好好分析一下哈。我在get里面返回了data.name,那data.name的意思不就是要这个属性值吗,于是又跑到get函数里,再返回再调用...无限递归最后爆栈。set函数也是一样的,一直在修改data.name属性就一直访问set,最后爆栈(执行栈的概念可以去了解一下),这个可咋解决啊。
于是冥思苦想,诶!,我搞个中转变量不就好了吗,于是乎...
const data = {
name: 'xxx'
}
let tmp = data.name
Object.defineProperty(data, 'name', {
get () {
return tmp
},
set (newVal) {
tmp = newVal
}
})
欸!成了!第一步成功了,可这Vue也不是一个一个给对象属性来个Object.defineProperty啊,那肯定得封装一个函数吧,来吧,试一下
const data = {
name: 'xxx'
}
function defineReactive (target, key, val) {
if (arguments.length === 2) val = target[key]
Object.defineProperty(target, key, {
get () {
console.log('get')
return val
},
set (newVal) {
console.log('set')
if (newVal === val) return
val = newVal
}
})
}
defineReactive(data, 'name')
咱把刚刚的中间变量变成函数的val形参,到这好像还是这么回事哈,我确实实现了数据的监测吧。
递归实现数据监测
可Vue肯定也不能一个一个给对象属性都调一次defineReactive吧,那再封装一个函数可不好使了,也没有代码可以封装了,这可咋整,欸!那这对象的属性我全拿出来再遍历给他们一个一个都加个defineReactive,这可不就解决了吗,开干!
const data = {
name: 'xxx',
age: 19,
major: 'student'
}
function defineReactive (target, key, val) {
if (arguments.length === 2) val = target[key]
Object.defineProperty(target, key, {
get () {
console.log('get')
return val
},
set (newVal) {
console.log('set')
if (newVal === val) return
val = newVal
}
})
}
function Observer (value) {
const keys = Object.keys(value)
keys.forEach(el => {
defineReactive(value, el)
})
}
Observer(data)
一看结果,这可太棒了啊,我这每一个属性都有get和set为它们服务,这不就成功了吗,可这Vue可以实现多层的数据监测啊,我这好像只能最外面一层实现呢,那这我擅长啊,递归呗,直接完活啊
const data = {
name: 'xxx',
age: 19,
major: 'student',
friends: {
f1: {
name: 'lz',
age: 20
},
f2: {
name: 'zl',
age: 21
}
}
}
function defineReactive (target, key, val) {
if (arguments.length === 2) val = target[key]
Object.defineProperty(target, key, {
get () {
console.log('get')
return val
},
set (newVal) {
console.log('set')
if (newVal === val) return
val = newVal
}
})
}
function Observer (value) {
const keys = Object.keys(value)
keys.forEach(el => {
if (value[el].constructor === Object) Observer(value[el])
defineReactive(value, el)
})
}
Observer(data)
一看这截图,这不是妥妥的响应式数据了吗,虽然我们现阶段只考虑对象哈,但是效果好像确实达到了啊,思维应该没啥错,但是我们得写的高级点啊,来吧,干活!
最后的代码优化
刚刚我们实现的雏形是不是很像在每个数据身上安个监控,那既然这样就单独给它一个监控的属性 ob,所以我们把Observer写成一个类。那么要添加一个属性,不就又是我们的老朋友了吗,那么这个属性我们肯定不希望它可以被枚举到,所以封装一个def函数,看代码!
function def (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
class Observer {
constructor(value) {
def(value, '__ob__', this, false)
this.walk(value)
}
walk (value) {
const keys = Object.keys(value)
keys.forEach(el => {
observe(value[el])
defineReactive(value, el)
})
}
}
或许你还是不太懂,没有关系,我们仔仔细细来捋一遍,之前写的Observer()函数就是用来给每个属性添加一个监控对吧,那么我们现在单独给一个__ob__用来放置这个监控(看到后面会知道为什么给个__ob__属性,现在不纠结),那么我们把这个函数重新写成一个类,这个类里面无非就是多了一块def()函数的调用(先不管下面的observe函数),这个def函数就是给这个属性加个监控,值为这个类的实例。然后就执行刚才一样的操作了,遍历对象让每个数据都是响应式的。
那么这个observe函数其实也是很简单的,你一看就能懂!
function observe (value) {
if (value.constructor !== Object) return
let ob
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__
} else {
new Observer(value)
}
return ob
}
如果某个属性身上已经有监控了,就不必再安一个监控了,直接赋值返回一顿操作,如果没有就去调用Observer对吧,给每个属性都安一个监控。
结语
因为我也还是学习阶段,希望我的理解可以帮助到刚刚接触vue的小伙伴们,也希望大家可以指出我理解中的错误,让我更上一层楼啦!😊😊😊😊