看个栗子:
const myData = {
n : 0
}
console.log(myData)
new vue({
}).$mount('#jeff')
声明一个 mydata
对象, consolog.log
之后如下:
但是如果把 mydata
传入vue实例里则如下:
const myData = {
n : 0
}
console.log(myData)
new vue({
data:myData
}).$mount('#jeff')
则打印出来的 mydata
为:
n的值为什么会从数字 0
变成 (...)
一开始是 {n:0}
,传给 new Vue
之后立马变成 {n:(...)}
,这是什么东西?
在解释之前先了解ES6的 getter和setter
getter 和setter
举个栗子
let obj0 = {
姓: "高",
名: "圆圆",
age: 18
};
需求一,得到姓名
let obj1 = {
姓: "高",
名: "圆圆",
姓名(){
return this.姓 + this.名
},
age: 18
}
console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?
需求二,姓名不需要括号也能得出值
let obj2 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
age: 18
};
console.log("需求二:" + obj2.姓名);
// 总结:getter 就是这样用的。不加括号的函数,仅此而已。
需求三:姓名可以被写
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx){
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
};
obj3.姓名 = '高媛媛'
console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)
// 总结:setter 就是这样用的。用 = xxx 触发 set 函数
但。。。这和 (...)
有什么关系?
可以把需求三打印出来console.log(obj3)
如图
看,也出现 (...)
,说明之前的栗子里出现的 (...)
,也是一个getter和setter。
而且在代码里,并没有 姓名:(...)
,在浏览器里却出现了,说明可以对这个不存在的属性进行读和写。
读和写的操作就是getter和setter。
Object.defineProperty
这是ES6新语法:官方文档
以刚刚的需求三为栗子:
如果我想对需求三添加一个新的getter和setter如下:
let _xxx = 0
Object.defineProperty(obj3, 'xxx',{
get(){
return _xxx
},
set(value){
_xxx = value
}
})
xxx
是随便起的虚拟属性,意思是给obj3
添加一个xxx
的属性_xxx
是存放xxx
的属性的
小结:可以给对象添加属性value,并且给对象添加getter/setter,然后对属性的读和写进行监控。
代理和监听
看栗子:
基础
let data0 = {
n: 0
}
需求一:用Object.defineProperty定义n
let data1 = {}
Object.defineProperty(data, 'n',{
value: 0
})
console.log(`需求一:${data1.n}`)
//为什么我要写这么复杂的语法,就不可以直接n:0呢?
需求二:n 不能小于 0,即 data2.n = -1 应该无效,但 data2.n = 1 有效
let data2 = {}
data2._n = 0 // _n 用来偷偷存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)
//那如果对方直接使用 data2._n 呢?
需求三:使用代理
let data3 = proxy({data:{n:0}}) //括号里是匿名对象,无法访问
function proxy({data/*使用了解构赋值*/}){
const obj = {}
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value < 0)return
data.n = value
}
})
return obj //obj就是代理
}
// data3 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)
//如果把匿名函数改一下。。
说明上面的解构赋值:
//完整写法是
function proxy(opction){ //接受一个opction参数。
const data = opction.data;
}
//解构赋值写法
function proxy(opction){ //接受一个opction参数。
const {data} = opction;
}
//简化解构赋值的写法
function proxy({data}){
//直接省略
}
:::success 什么是代理?(设计模式)
- 对
myData
对象的属性读写,全权由另一个对象vm
负责。 - 那么
vm
就是myData
的代理。 - 比如
myData.n
不用,偏用vm.n
来操作myData.n
:::
需求四:
let myData = {n:0}
let data4 = proxy({data:myDate}) //括号里是匿名对象,无法访问
。。。。。。
// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 失败了吗!?`)
//杠精居然成功了,可以使data4小于0。还有什么办法呢?
需求五:如何拦截杠精,就算用户擅自修改 myData,也要拦截他
let myData5 = {n:0}
let data5 = proxy2({data:myData5})
function proxy2({data}){
let value = data.n
Object.defineProperty(data, 'n',{
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 就是代理
})
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)
这代码看着眼熟吗?
let data5 = proxy2({ data:myData5 })
let vm = new Vue({data: myData})
现在我们可以说说 new Vue 做了什么了
vm = new Vue({data: myData})
- 会让
vm
成为myData
的代理(proxy) - 会对
myData
的所有属性进行监控 - 如果
mydata
的属性变了,就可以调用render(data)
。
如图:
什么是响应式?
若一个物体能对外界的刺激做出反应,那么就是响应式。
而Vue的data是响应式,即通过 Object.defineProperty
来实现数据响应式。
bug
Vue有个小bug,是Object.defineProperty
问题。
假如,有个比较水的前端工程师没有给 'n'
怎么办?必须要有一个 'n'
才能监听&代理 obj.n
才行,如下代码:
new Vue({
data: {},
template: `
<div>{{n}}</div>
`
}).$mount("#app");
data里没有 'n'
属性,Vue只会提出警告。
这时,有个方法可以绕过这个警告
new Vue({
data: {
obj: {//vue的警告只会检查第一层,也就是这里。
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1; //请问,页面中会显示 1 吗?
}
}
}).$mount("#app");
因为vue的警告只会检查第一层,所以只需要在data下再声明一个对象obj,然后在obj对象里存放数据,就会绕过vue的警告。这是一个bug。
因此调试错误很难发现问题。
解决方案:
使用vue提供的api —— Vue.set
和 this.$set
,代码如下:
new Vue({
。。。。。。。。
methods: {
setB() {
Vue.set(this.obj, 'b', 1 ) //请问,页面中会显示 1 吗?
//or
this.$set(this.obj, 'b', 1)//和上面一样的
}
}
}).$mount("#app");
这两个api的作用:
- 新增key
- 自动创建代理和监听,前提没有创建过。
- 触发UI更新(但不会立即更新)
因为Vue没办法事先监听和代理,所以使用set来新增key,自动创建代理和监听,UI更新。
建议:最好事先把属性写出来,不要新增key。
数组变异
栗子:
new Vue({
data: {
array: ["a", "b", "c"]
},
template: `
<div>
{{array}}
<button @click="setD">set d</button>
</div>
`,
methods: {
setD() {
this.array[3] = "d"; //请问,页面中会显示 'd' 吗?
// 等下,你为什么不用 this.array.push('d')
}
}
}).$mount("#app");
- 数组,长度可以一直增加,下标是key。没办法提前把数组的key都声明出来。
- Vue也不能检测并新增下标。
- 要是每次都使用
Vue.set
或者this.$set
那也太傻了。
解决办法
使用Vue提供的篡改数组api,见官方文档:数组检测更新
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
例如:
new Vue({
data: {
array: ["a", "b", "c"]
},
template: `
<div>
{{array}}
<button @click="setD">set d</button>
</div>
`,
methods: {
setD() {
this.array.push("d");
}
}
}).$mount("#app");
这样就可以解决问题了。但又有了新的问题:上面Vue提供的7种方法和JS的有什么区别?
查看开发者工具,如图:
和JS的不一样。而JS的七种方法在 __proto__:Array
下面。
如果使用了Vue提供的这七种api会做两件事情:
- 先调用JS对应的方法
- 然后在自动创建监听和代理。
其实就是添加了一个原型链,没有vue之前原型链是直接是Array对象,有了vue之后在连接Array对象直接新增一个vue的api对象。