一、什么是数据响应式
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。——《Vue.js 技术揭秘》
上图是Vue的官方文档中的图解,黄色部分是 Vue 的渲染方法,视图初始化和视图更新时都会调用render 方法进行重新渲染。渲染时不可避免地会 touch 到每个需要展示到视图上的数据(紫色部分),触发这些数据的 get 方法从而收集到本次渲染的所有依赖。而当我们在修改这些收集到依赖的数据时,会触发数据中的 set 属性方法,该方法会修改数据的值并 notify 到依赖到它的观察者,从而触发视图的重新渲染。
二、将数据变成响应式
1. getter,setter
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
let obj = {
firstName: "刘",
lastName: "诗诗",
get name() {
return this.firstName + this.lastName;
},
set name(value){
this.firstName = value[0]
this.lastName = value.slice(1)
},
age: 18
};
obj.name = '刘诗诗'
console.log( obj.name);
如上例,getter、setter方法也可以是obj对象的属性,我们可以使用操作属性的点语法来获取name和修改name。如果在控制台进行打印就会发现 name属性下的getter、setter方法,就和实例化vue中的data 里的值n打印出来变为了n:(...)也有getter、setter方法一样,说明vue重写数据n,将其转换为getter/setter的对象属性。
2. Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。——MDN
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty() 接收三个参数:第一个是要定义属性的对象;第二个是要定义或修改的属性的名称或 Symbol;第三个则是要定义或修改的属性描述符。在第三个参数中,我们可以定义属性的 getter 函数和setter 函数,这便解释了Vue如何通过Object.defineProperty 把这些 data里的属性全部转为 getter/setter。
getter和setter对属性的读写操作进行了监控,但是我们只要知道属性名,就可以绕过getter和setter方法直接对属性进行控制,Vue利用代理模式解决了这个问题。
3.proxy
let myData = {
n: 0
}
let data = proxy({
data: myData
}) // 括号里是匿名对象,无法访问
function proxy({ data } /* 解构赋值 */ ) {
//for循环省略
let value = data.n
Object.defineProperty(data, 'n', {
get() {
return value
},
set(newValue) {
value = newValue
}
})
const obj = {}
Object.defineProperty(obj, 'n', {
get() {
return data.n
},
set(value) {
data.n = value
}
})
return obj // obj 就是代理
}
上面的方法对每个传入的数据新增 getter/setter,此后原始的数据就会被 getter/setter 所替代,相当于复制了原始数据,这样不管是操作 let data = proxy(data); 中的 data,还是操作 myData,都会被我们的 getter/setter 所拦截。
经过代理后的代码是不是就和实例化一个vue对象的代码很相似了。
const vm = new Vue({ data: {} })
let data = proxy({data:myData})
所以,new Vue时Vue会遍历传入的data对象所有属性,并使用Object.defineProperty把这些属性全部转为getter/setter,这样就生成一个新的对象全权负责数据——就是实例化的Vue对象vm。这样vm会成为data 的代理,对 data 的所有属性进行监控,当数值发生改变的时候,vue就调用render函数重新渲染视图。
三、Vue 数据响应式的 Bug
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。——Vue文档
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。而例子中的b属性是在实例化外再新添的,所以vue不能对b值的变化在视图上做出响应。但很多时候我们并不能事先知道data的所有属性,如果要新添属性怎么办呢?
对于对象
可以使用 Vue.set(object, propertyName, value) 方法向对象添加响应式 property。
Vue.set(vm.data, 'b', 2)
还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.data,'b',2)
对于数组
var vm = new Vue({
data: {
items: ['a', 'b', 'c','d']
}
})
vue对数组进行了改变,给数组加了一层原型,在其中Vue修改了7个方法覆盖了之前数组原型的7个方法。调用这些Vue新定义的方法时,在这些新方法里Vue会加上对新添的元素的监听(相当于进行了set操作),把新数据也进行代理,这样vue就能重新监测到数组的变化了更新UI操作。
具体的七个变更方法:(这些原始方法正好也会返回一个新数组)
push()pop()shift()unshift()splice()sort()reverse()