Vue中的数据响应式
在Vue的使用中,我们通过对组件中data的数据进行设置,从而实现页面的更新渲染。
一、什么是数据响应式
在Vue中,一个组件 data 的数据一旦变化,立刻触发视图的更新,这就是Vue的数据响应式,它也是实现数据驱动页面渲染的第一步。而data的本质是一个对象,我们通过为对象添加属性名(页面中的变量名)及属性值(变量值)的方式来声明页面中的数据。那么我们如何实现数据响应呐?在这个问题之前,我们先来了解一下data对象中的每个属性(页面中的每个变量)。
二、Object.getOwnPropertyDescriptor( )
该方法返回一个对象,该对象描述给定对象上特定属性的配置。
我们可以通过这个方法先来了解一下在普通JS中data对象每个属性的具体配置信息。例如:我们在JS文件中声明一个data对象,并在这个data对象中声明一个name属性,赋值为‘张三’。
const data={
name:'张三'
}
console.log(Object.getOwnPropertyDescriptor(data,'name'));
那么我们就可以通Object.getOwnPropertyDescriptor(data,'name')的方法来获取name属性的配置信息。
在浏览器的控制台中,我们可以看到,name属性中有4个配置信息,分别是configurable(是否可删)、enumerable(是否可枚举)、writable(是否可写)以及value(属性值)。那么我们如何修改属性的配置信息,这又和Vue的数据响应式有什么关系呐?这就得聊聊Object.defineProperty()。
三、Object.defineProperty()
该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
通过这个方法,我们可以对上述的属性的配置项进行修改。例如,我们先将writable(是否可写)设置为false,然后通过data.name='李四'的方式尝试修改name的属性值。
const data={
name:'张三'
}
Object.defineProperty(data,'name',{
writable:false //将name属性设置为不可写
})
data.name='李四'
console.log(Object.getOwnPropertyDescriptor(data,'name'));
在浏览器中,我们会发现,name的属性值由于设置为不可写,因此属性值依然为‘张三’。
四、set和get方法
那么,这和数据响应有什么关系呐?我们可以使用这个方法给属性添加一个set和get方法,从而实现1个对象的数据响应式。例如,我们为name属性配置set和get方法,当我们读取数据的时候,会触发Object.defineProperty()中的get()方法,并将其return的返回值作为读取的数据;同理,我们在设置数据时,会触发Object.defineProperty()中的set()方法,通过这个方法来修改属性的值。
const data={
name:'张三'
}
//使用变量来接收一下name.data,防止死循环
let username=data.name
Object.defineProperty(data,'name',{
get: function() {
console.log('get触发')
return 123
},
set: function(newVal) {
console.log('set接收到的newVal:',newVal)
username = newVal
}
})
// 修改属性值
data.name='李四'
// 读取属性值
console.log('get返回结果:',data.name);
console.log(Object.getOwnPropertyDescriptor(data,'name'));
在页面中,我们读取到的数据是:123(get方法中的返回值,这里只是举例,实际中我们返回的是真实数据,即案例中的username),并且将data中的name属性赋值为'李四'。
五、单一属性的数据响应
有了get和set方法,我们就可以实现对data中属性的自由操作,那么应该如何让修改的数据驱动页面的更新,从而实现单一属性的数据响应那?这里我们需要借助document.getElementById和innerHTML来现实。例如,我们可以封装一个函数叫做setView(),用来同步视图的数据,并且在set方法中调用它。这样每次数据更新之后,setView()方法被触发,改变页面中的视图,从而实现数据响应式。
<body>
<div>
<p id="name"></p>
<p id="age"></p>
</div>
<script>
// 定义数据
const data = {
name: '张三',
age: 19
}
// 同步视图的方法
function setView() {
document.getElementById('name').innerHTML = data.name
document.getElementById('age').innerHTML = data.age
}
setView()
// 利用循环对数据进行监听
Object.keys(data).forEach(key => {
// 利用外部数据进行数据的读写控制
let getValue = data[key]
Object.defineProperty(data, key, {
// 读数据
get() {
return getValue
},
// 写数据
set(val) {
getValue = val
setView()
}
})
})
</script>
</body>
六、多个属性的数据响应
通过上述的方法,我们实现了data对象中单一属性的数据响应,但是我们可以发现Object.defineProperty()中每次只可以操作data对象中的1个属性。
那么我们如何实现多个属性的数据响应呐?并且,如果属性值对象,我们又应该如何处理?其实,对于多个属性,我们可以使用for(key in obj)的方式来实现遍历data对象中的属性,而对于属性值为对象的属性,我们则可以通过‘递归函数’的方式来解决。
<body>
<div>
<p id="name"></p>
<p id="age"></p>
<p id="idCard"></p>
<p id="address"></p>
</div>
<script>
// 数据
const data = {
name: '张三',
age: 19,
info: {
idCard: 233455667889001223,
address: 'XXX'
}
}
// 同步视图的方法
function setView() {
document.getElementById('name').innerHTML = data.name
document.getElementById('age').innerHTML = data.age
document.getElementById('idCard').innerHTML = data.info.idCard
document.getElementById('address').innerHTML =data.info.address
}
setView()
observer(data)
// 递归的方法
function observer(data) {
//Object.keys(data).forEach()等同于for(key in Obj)
Object.keys(data).forEach(key => {
//判断是否为复杂型数据,Arrary和Object通过typeof返回的都是Object
if (typeof data[key] === 'object') {
//如果是,则递归调用自己,并且将递归的结果返回
return observer(data[key])
}
//赋值中间变量,防止死循环
let getValue = data[key]
Object.defineProperty(data, key, {
get() {
return getValue
},
set(val) {
getValue = val
//同步视图的方法
setView()
}
})
})
}
</script>
</body>
以上就是实现数据响应式的方法。在Vue中,也是类似的,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
并且每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
七、Vue中数据不响应的问题
在Vue中,如果我们单纯的对某一个数组中的单个数据进行修改,或者为某一个属性值类型为对象的数据添加新的属性,此时通过data.obj.key=val的形式赋值,并不会实现页面的数据响应式。原因分为2个方面:
1、为属性值为对象类型的属性添加新属性
注意,这里我们说的是添加新的属性,而不是修改现有的数据。造成这一问题的根本原因是Object.defineProperty()方法自身的一个缺陷,这个方法只能对一个对象上现有的属性进行监听拦截,而不能对新增的属性进行监听拦截。
当我们的页面初次加载时,就已经触发了Object.defineProperty()对属性进行监听;之后我们再为对象添加新的属性,只要页面不触发Object.defineProperty()方法,就无法让Object.defineProperty()对新添加的属性进行监听,从而实现数据响应式。
2、通过下角标的方式修改数组中的某一个元素
其实,对数组中现有的某一元素进行修改,Object.defineProperty()是可以进行数据监听拦截,并实现数据响应式的,但是Vue的官方团队放弃这一功能,因为数组的长度是不固定的,这可能会引起性能代价与用户体验不成正比。在GitHub中,Vue的作者尤大大也是这样回应使用者的。
3、如何解决
上述的两个问题,我们可以通过this.$set(obj, key, value)的方式来解决。另外,也可以在修改数组中某一现有元素或者为对象添加新的属性之后,正常的再修改另外一个数据,这样就会引起页面的刷新,从而让Object.defineProperty()获取最新的属性。即使第二种方法也可以实现所需的效果,但我们还是推荐使用第一个方式。