当一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。这些 getter/setter 对用户是不可见的,但它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
Vue 如何实现响应式
getter/setter
getter/setter 用于对属性的读写进行监控
let obj = {
lastName: '张',
firstName: '三',
get name(){
return this.lastName + this.firstName
},
set name(name){
this.lastName = name[0],
this.firstName = name.substring(1)
}
}
obj.name //'张三'
obj.name = '李四'
console.log(obj.lastName) //李
console.log(obj.firstName) //四
Object.defineProperty
Object.defineProperty
用于给对象添加新属性,也可以给对象添加 getter/setter
let obj = {}
Object.defineProperty(obj, 'x', {value: 1})
Object.defineProperty(obj, 'y', {
get(){...}
set(value){...}
})
监听与代理
let myData = {n: 0}
let newData = proxy({data: myData}) //括号里是匿名对象,无法访问
function proxy({data}){ //结构赋值
//监听data
let value = data.n
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
value = newValue
}
})
//代理
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return value
},
set(newValue){
value = newValue
}
})
return obj //obj 就是代理
}
vm = new Vue({data: myData})
就做了和上面代码类似的事情。
vm
成为了myData
的代理- 会对
mydata
的所有属性进行监控
目的: 无论直接修改 myData.n
还是修改 vm.n
,vm
都会收到通知,然后调用触发重新渲染
Vue 对 methods 和 computed 也有类似处理。
data 的对象
Vue2 通过 Object.defineProperty(obj, 'n', {...}
来实现数据的响应式,data
中必须有 'n'
才可以监听&代理 obj.n
,若无 'n'
,就会产生问题
//无 data.n
new VUe({
data: {},
template: `
<div>{{n}}</div>
`
}).$mount("#app")
无 data.n
或 data.n
的值为 undefined
,此时 n 被引用,不会有显示,控制台会报警告
//有 data.obj,但无 data.obj.n
const vm = new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.n}}
<button @click="setN">set n</button>
</div>
`,
methods: {
setN() {
this.obj.n = 1;
}
}
}).$mount("#app");
data.obj
中开始并没有 n
,后面运行 vm.obj.n = 1
并不会让 n
出现在页面中,因为 vm
没有监听&代理 data.obj.n
解决办法:
- 在开始就定义好
data.obj.n = undefined
(声明响应式 property,推荐) - 使用
Vue.set
或者this.$set
methods: {
setN(){
Vue.set(this.obj, 'n', 1)
//或
this.$set(this.obj, 'n', 1)
}
}
一个特殊例子
//html
<div id="app">
<span class=span-a>
{{obj.a}}
</span>
<span class=span-b>
{{obj.b}}
</span>
</div>
js
//js
var app = new Vue({
el: '#app',
data: {
obj: {
a: 'a',
}
},
})
app.obj.a = 'a2'
app.obj.b = 'b'
最终 span-a 中会显示 a2,span-b 中显示 b。
这是因为视图更新是异步的,a1 变成 a2 时,Vue 监听到这个变化,并不会马上更新视图,而是创建一个视图更新任务到任务队列里。然后继续运行代码 app.obj.b = 'b'
。视图更新时,Vue 会去做 diff,发现 a 和 b 都变了,于是去更新 span-a 和 span-b。
data 中的数组
Vue 不能检测数组的以下变动:
- 利用索引直接设置一个数组项
- 修改数组长度
let vn = new Vue({
data: {
array: ["a", "b", "c"]
}
})
vm.array[1] = 'x' // 不是响应性的
vm.array.length = 2 // 不是响应性的
以上代码并不会触发对 array 的更新。
解决方法:
-
如何设置数组项:
使用
Vue.set
或者vm.$set
,可以实现vm.array[1] = 'x'
相同的效果,也将在响应式系统内触发状态更新。 -
如何修改数组长度
可以使用
splice
:vm.array.splice(2)
但 Vue 中的
splice
是被篡改过的,并不是 JS 中的数组的splice
。Vue 篡改过的方法,称为变异方法。数组变异方法有 7 个:
push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
数组新增修改最好通过这 7 个 API