前言
数据响应式是Vue非常具有代表性的特点,在开发者修改数据后,Vue会根据数据来更新网页视图,这就给开发者提供了简单直接控制视图的工具。 不过这里面有一些小坑,知道一点原理,可以帮助我们少写一些bug。
什么是Vue的数据响应式?
上代码:
const vm = new Vue({data:{n:0}})
如果修改了vm.n,那么网页UI中的n就会变化。Vue是通过Object.defineProperty来实现数据响应式的。
这里面其实有三个步骤
- 对象传入Vue实例作为
data选项 - Vue 将遍历此对象所有的选项,并使用
Object.defineProperty把这些 选项全部转为 getter/setter。
gette/setter
getter和setter可以设置在对象上,并对其数据进行读写。
先看代码:
let obj1 = {
firstName: "小",
lastName: "白菜",
get fullName() {
return this.firstName + this.lastName;
},
age: 18
};
console.log(obj2.fullName);
//输出结果:小白菜
//getter可以不加括号调用函数
let obj2 = {
firstName: "大",
lastName: "铁柱",
get fullName() {
return this.姓 + this.名;
},
set fullName(xxx){
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
};
obj2.fullName = '大铁柱'
console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
//需求二:需求二:姓 大,名 铁柱
//getter用 fullName = xxx 来触发函数
一句话总结:
getter就是不加括号的函数,使用obj2.fullName可以直接调用;setter接受一个参数, 用fullName = xxx来触发函数,并改变里面的值。
以上代码我们可以看出 getter 和 setter 都是声明时直接使用的,但我想声明后使用怎么办?接下来就轮到 Object.defineProperty() 出场了。
Object.defineProperty()
极简语法:Object.defineProperty(需要定义的对象, 定义的东西是什么, {getter/setter})
Object.defineProperty()的作用有三个:
- 给对象添加属性
value; - 给对象添加
getter/setter; getter/setter用于监听属性的读写。
配合代码食用更好理解:
let data = {}
data._n = 0 // _n 用来存储 n 的值
Object.defineProperty(data, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
这里需要注意的是n是不存在的,并且不能直接通过get()来传递,否则会造成死循环。所以需要重新定义一个值来传。
但是,data._n暴露在外面,可以直接进行修改,这不是我们想要的,那么应该怎么做呢?
这里就需要用到一个代理了。
什么是代理?如何实现?
同样,先看代码理解:
let data0 = proxy({ data:{n:0} }) // 括号里是匿名对象,无法访问
function proxy({data}/* 解构赋值 */){
const obj = {};
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
以上代码块中存在解构赋值,原始代码如下:
function proxy(options){
const {data} = options //这里也是解构
function proxy({data}){} //这样
}
这样一来,如果对方想修改数据,只能通过obj来进行修改。
其实想要修改数据,还有另一种方法。只需要把匿名函数中的对象提取出来,并赋值给另一个变量即可:
let myData = {n:0}
let data = proxy({ data:myData })
这时候,我们不仅仅需要依靠代理,还需要再加一个监听。就算修改了myData,也可以拦截:
let myData = {n:0}
let data0 = proxy2({ data:myData }) // 括号里是匿名对象,无法访问
function proxy2({data}){
let value = data.n //把data.n复制到value上
delete data.n //这句可以省略,因为下面的语句会直接覆盖data.n
Object.defineProperty(data, 'n', { //这里挂载了data
get(){
return value
},
set(newValue){
if(newValue < 0)return
value = newValue
}
})
// 加了上面几句,即可监听 data
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
以上代码相当于把原来的数据复制了一遍,然后删掉旧数据,用新数据来填补,这样就可以防止通过对象来修改数据了。 接下来就容易理解我们在创建一个 Vue 实例时,数据响应式是如何工作的了。
new Vue做了什么?
对比一下这两个代码,是不是有内味了?
let data = proxy({ data:myData })
let vm = new Vue({data: myData})
而在传入data的时候,Vue到底做了点啥呢?来看代码:
const Vue = window.Vue
const myData = {
n:0
}
new Vue({
data: myData,
template: `
<div>{{n}}</div>
`
}).$mount('#app') // 挂载到index.html里面的div节点上
setTimeout(() => {
myData.n += 10
}, 0)
console.log(myData)
控制台打印结果如下:
{__ob__: we}
n: (...)
__ob__: we {value: {...}, dep: ce, vmCount: 1}
get n: f ()
set n: f (t)
__proto__: Object
在以上代码中,vm = new Vue({data: myData})能让vm成myData的代理,监控myData所有属性。打个比方,vm和myData类似于房东和中介的关系。
vm监听myData的变化,主要用于调用render(data),让UI变化,重新渲染网页。
总结
- getter和setter可以设置在对象上,并对其数据进行读写;
- Object.defineProperty()是Vue实现数据响应式的主要方式;
- 为了不让外部直接修改对象参数,Vue需要给对象添加代理和监听;
- Vue让vm成为myData的代理,开发者对vm的操作就是对myData的操作,并且还能通过this来访问vm;
- Vue需要监听myData属性,一旦有变就会即时响应并刷新UI。