Vue的数据响应式
代理和监听
今天深入理解options.data
使用的是完整版的Vue做演示
先看一段代码
const myDate = {
n: 0
}
console.log(myDate)
const vm = new Vue({
data: myDate,
template: `
<div>
{{n}}
<button @click="add">+10</button>
</div>
`,
methods: {
add() {
this.n += 10
}
}
}).$mount("#app");
setTimeout(() => {
myDate.n += 10
console.log(myDate)
}, 3000)
打印出来的两个东西。
get的计算属性
get怎么用,就是在方法面前加get,然后调用的时候就可以不加括号。
有get那就有set
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名(){
return this.姓 + this.名
},
set 姓名(xxx) {//xxx是名字
this.姓 = xxx[0]
this.名 = xxx.substring(1)
},
age: 18
}
console.log(obj3.姓名)//就可以打印出高圆圆,不加括号,用了get
obj3.姓名 = '刘诗诗'//就可以改变姓和名,触发了set函数
console.log(obj3);//把obj3打印出来
最后我们把obj3
打印出来可以看到,这个姓名和之前的n差不多,(...)
,都长这样
说明之前得到的n
也是一个get
和一个set
,姓名
和这个n
一样,并不是真实的属性
就是不存在真实的姓名
,但是你可以对姓名
读和写 通过get和set来完成
上面的get和set
都是在对象创建的时候就写好的,如果我们想在对象创建后再去添加get和set
这个时候就用
Object.defineProperty(obj3, 'xxx',{
get(){},
set(){}
})
意思是在obj3
上面创建了一个xxx
是属性,里面有get和set,在上面的get和set上不能使用xxx
再看下面代码
let data0 = {
n: 0
}
let data1 = {}
Object.defineProperty(data1, 'n', {value: 0})
//给data1定义一个属性n,n的值是0
//上面的两种操作看似效果一样,不过当我们有其他需求时,就不一样了。
//需求:n不能小于0
//即data2.n = -1因该无效,但data2.n = 1有效
let data2 = {}
data2._n = 0//因为defineProperty里面定义的属性,在get和set还不能使用该属性,所以定义一个存储值的东西_n
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0)
return
this._n = value
}
})
console.log(`需求二:${data2.n}`)//0
data2.n = -1
console.log(`需求二:${data2.n}`)//打印出0,设置成-1失败,还是原来的值
data2.n = 1
console.log(`需求二:${data2.n}`)//打印出1,设置成1成功
//上面的也有一个问题,虽然data2.n不能变成负值,但是data2._n可以直接改
//需求:使用代理
let data3 = proxy({ data: {n:0} })//括号里是匿名对象,无法访问。
function proxy({data}){ //解构赋值,这个data是上面的data的引用
const obj = {}
Object.defineProperty(obj. 'n', {
get(){
return data.n
},
set(value) {
if(value<0)return
data.n = value
}
})//对obj的n做什么,就对data.n做什么
return obj//obj就是代理
}
console.log(`需求三:${data3.n}`)//0
data3.n = -1
console.log(`需求三:${data3.n}`)//打印出0,设置成-1失败,还是原来的值
data3.n = 1
console.log(`需求三:${data3.n}`)//打印出1,设置成1成功
//这样就没有暴露出任何可以让你直接修改的属性
//上面是匿名对象{n: 0}
//下面改成不是匿名对象
let myData = {n: 0}
let data4 = proxy({ data: myData })//
//就可以直接使用myData.n = -1,像这样直接设置
//需求: 就算直接修改myData.n,也要拦截他
let myData5 = {n: 0}
let data5 = proxy2({ data: myData5 })
function proxy2({data}) {
let value = data.n//用value来存储原始的值,
//delete data.n//把原始数据的n删掉
//上面这句话可以不写,因为下面的添加'n'属性,发现重名了,就会把上面的n直接覆盖掉。
Object.defineProperty(data. 'n', {
get(){
return data.n
},
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就是代理
}
//这样的话,外面的n就被删除了(覆盖),就不能直接赋值了
console.log(`需求5:${data5.n}`)//0
data5.n = -1
console.log(`需求5:${data5.n}`)//打印出0,设置成-1失败,还是原来的值
data5.n = 1
console.log(`需求5:${data5.n}`)//打印出1,设置成1成功
把上面的代码细品一下
let data5 = proxy2({ data:myData5 })
const vm = new Vue({data: {n:0} })
发现和Vue实例创建的时候差不多。
所以最开始看到的打印出来的对象,Vue的原理就是这样的。
vm = new Vue({data:meData})
- 会让
vm成为myDate
的代理 - 会让
myData
的所有属性进行监控 - 为啥要监控,防止
myData
的属性变了,vm不知道 - vm知道了,就可以
UI = render(data)
就是把myData
的值全部换了一个地方,换到了vm自身身上。不能通过myData
来修改。
返回第一张图,第一段代码,两个console.log
的地方很精髓
传入之后myData就发生了变化,Vue把myData篡改了。
原始的data.n没有被删掉,只是在改这个对象里面的东西
一个是代理,一个是被修改过后的原始对象
这里Vue有个问题,算小bug吧
上面说的Vue在初始化的时候使用了Object.defineProperty(obj, 'n',{...})
所以必须给了n
才能去监听n
,如果没有给n
,怎么监听呢
new Vue({
data: {},
template: `
<div>
{{n}}
</div>
`
}).$Mount("#app")
页面不会打印出n
,并且抛出一个警告,没有n
new Vue({
data: {
obj: {
a: 0
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1;
}
}
}).$Mount("#app")
这样写就不会报错,因为Vue监听了第一层的obj,点击按钮,页面会出现obj.b吗?
不会出现,因为在最开始的时候,Vue只是监听了obj.a,没有监听obj.b,所以没有反应。
解决
- 可以事先声明好,赋值
undefined
,后面再加属性就行了 - 使用
Vue.set
或者this.$set
methods: {
setB() {
//this.obj.b = 1;
Vue.set(this.obj, 'b', 1);
this.$set(this.obj, 'b', 1)
}
}
Vue.set
和this.$set
,就相当于调用了Object.defineProperty(obj, 'n',{...})
,就对obj.b进行了监听
- 新增key
- 自动创建代理和监听(如果没有创建过)
- 出发UI更新(但不会立刻更新)
其实就是最开始调用Object.defineProperty(obj, 'n',{...})
,不过是,最开始没监听上,后面再调用这个去就监听
代理是写在vm
上的,监听是改当前的data
对象。
data里面有数组怎么办
数组的长度可以一直增加,下标就是key
array: ["a", "b", "c"]
等价于
array: {0: 'a', 1: 'b', 2: 'c'}
所以直接用
methods: {
setD() {
this.array[3] = 'd';
}
}
来向数组里面增加东西是没有用的。
只能通过
methods: {
setD() {
this.$set(this.array, 3, 'd');
}
}
数组提前声明是没用的,不知道有多少项,但是每次都写this.$set(this.array, 3, 'd')
又太麻烦。
Vue作者就提供了使用push
this.array.push("d");
就可以直接往数组里加东西了,但是这个push
,不是原来数组对象上的push
,而是被Vue的作者改了的push
来看看他是怎么改的呢
methods: {
setD() {
this.array.push("d");
console.log(this.array);
}
}
从打印出的对象可以看出,和之前的对象的方法不一样了,这就是Vue作者在array
对象和array
原型中间加了一层原型,这个push
是Vue作者自己写的。所以可以直接push
进数组里面
改了的push也很简单
- 调用原来的
push
- 告诉Vue,添加监听这个新
push
进来的东西
class VueArray extends Array {
push(...args) {
super.push(...args)//调用父类的push
//添加监听的操作,自己的代码
}
}
中间加的代码就是对push
篡改了,中间加了一层自己写的一些代码。
这就是大概思路。
上面是es6的写法,es5的写法要难一点
const vueArrayPrototype = {
push: function(){
//自己加的代码
return Array.prototype.push.apply(this, arguments)//es6的写法简单,es5的写法难一点,调用数组对象本身的push,然后改this的指向
}
}
vueArrayPrototype.__proto__ = Array.prototype
es5的大概思路是这样
总结
对象中新增的key
- Vue没有办法事先来监听和代理
- 可以用set来新增key,创建监听和代理,更新UI
- 但是数组做不到(不去新增key),
this.$set
又很麻烦
数组中新增key
- Vue的作者篡改了原型,来让我们能使用
push
等方法,直接改变data
中的数组
这些api在Vue.js文档中的变异方法
中