Vue2文档中的内在一节的深入响应式原理中所述,“Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。”我们来看看数据更新的常见的问题。
一、数据响应式常见问题
用户更改数据,视图自动刷新,UI响应数据的变化。const vm = new Vue({data: {n: 0}}),如果修改data,会导致UI中的n响应,就是响应式的。
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
let vm = new Vue({
data: {
obj: {
a: 1,
message: undefined
},
},
template:`<div>{{obj.message}}</div>`
}).$mount("#app");
vm.obj.message = 'xxxxx'
可以看到确实可以看到页面中变为xxxxx了,如果想要看到初始的message可以加个setTimeout来看。那是不是我们的message改变都能监听到并且作出响应呢?看下面的例子:
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
let vm = new Vue({
data: {
obj: {
a: 1,
},
},
template:`<div>{{obj.message}}</div>`
}).$mount("#app");
vm.obj.message = 'xxxxx'
可以看到并没有响应,当obj中初始没有message是,我们直接对obj加上message属性页面是不会被监听并且响应的。这是由于Vue的数据响应式是基于Object.defineProperty,而对象属性的添加和删除无法被Object.defineProperty监听。
我们如何让Vue将它转化为响应式呢。第一种方法,在data对象声明中考虑到要添加的。由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值,本例中可以把属性message设为undefined。
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
let vm = new Vue({
data: {
obj: {
a: 1,
message: undefined,
},
},
template:`<div>{{obj.message}}</div>`
}).$mount("#app");
vm.obj.message = 'xxxxx'
第二种方法是使用Vue提供了特定的方法Vue.set(object, propertyName, value) 向嵌套对象添加响应式 property。还可以使用vm.set的作用是新增响应式 property,自动创建监听和代理(通过Object.defineProperty给对象添加getter和setter,getter和setter用于对属性的读写进行监控),从而触发UI更新(异步)。
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
let vm = new Vue({
data: {
obj: {
a: 1,
},
},
template:`<div>{{obj.message}}</div>`
}).$mount("#app");
// Vue.set(vm.obj,'message','xxxxx')
vm.$set(vm.obj,'message','xxxxx')
vm.obj.message = 'xxxxx'
如果需要为数字元素重新赋值,可以使用 vm.$set(array, indexOfItem, newValue) 方法,当data中有数组很大时,不能提前声明所有的Key怎么办呢?
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {
array: ["a", "b", "c","d"]
},
template: `
<div>
{{array}}
<button @click="setE">set e</button>
</div>
`,
methods: {
setE() {
this.array[4] = "e";
}
}
}).$mount("#app");
我们按下set e的按钮不会有变化,UI并没有刷新,Vue通过 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些方法包括push()、pop()、shift()、unshift()、splice()、sort()和reverse()。你可以在Vue2教程中列表渲染的数组更新检测的变更方法找到他们。
我们可以将this.array[4] = "e"; 改为 this.array.push('e'),可以看到UI确实更新了。
setE() {
// this.array[4] = "e";
this.array.push('e')
}
我们将本例的array打印出来,发现它已经不是我们所熟知的数组了,这应该是尤大所说的变更方法吧。原型链中多了一层,其中正好是上面提及的7个方法。
为什么我们使用数组时,直接使用下标赋值无法被监听呢?这是出于性能来考虑的,对象和数组如果要监听每个元素,就是对其每个属性使用Object.defineProperty进行劫持,而数组监听key是以数组为数字下标的key,数组的数据量可能特别大,就可能非常耗费性能,Vue没有对元素的下标进行响应式处理,而是补充了7个变更的数组API,通过对数组原型链上的7个方法进行劫持,从而执行响应式处理。
二、关于Object.defineProperty
2.1、属性描述符
我们写一个普通的对象,但是这个普通对象对应的属性描述符可不仅仅只有一个value数据值哦,还有三个其他的特性:writable(可写)、configurable(可配置)和enumerable(可枚举)。
let myObject = {a: 2}
Object.getOwnPropertyDescriptor(myObject,"a")
我们可以通过下面方式来设置属性的值,但是只要的目的肯定不仅仅是设置值,而是为了修改其他三个值。
let myObject = {}
Object.defineProperty(myObject,'a',{
value: 2,
writable: true, // 是否可修改属性的值
configurable: true, // 是否可配置,configurable: false是单向操作不可撤销
enumerable: true // 是否可枚举
})
myObject.a
2.2、Getter和Setter
getter和setter可以改写默认操作,但是只能应用在单个属性上,getter会在获取属性值的时候调用,而setter会在设置属性值的时候调用,二者都是隐藏函数。当为一个属性定义了getter或者setter或者二者都有的话,JS会忽略value和writable的特性,而去关心get和set还有是否可枚举、可配置。
let myObject = {
get b(){return 2}
}
Object.defineProperty(myObject,'a',{
get: function(){return this.b * 2},
enumerable: true
})
myObject.b // 2
myObject.a // 4
myObject.a = 5 // 5
myObject.a // 4
可以通过对象文字语法get a(){}和defineProperty()显式定义。都会在对象中创建一个不包含值的属性,对这个属性的访问会自动调用这个隐藏函数。只设置getter没有设置setter,所以我们对a的值进行set操作会忽略赋值操作。我们设置了getter还应该设置setter才能让属性更合理。
let myObject = {
get a(){
return this._a_
},
set a(val){
this._a_ = val* 2
}
}
myObject.a = 2 // 2
myObject.a // 4
我们对a进行赋值操作时,setter会覆盖单a的默认的赋值操作。
重要参考: