参照源码版本:2.5.17
怪异问题:明明数据变了,为什么视图不更新?
相信大家刚接触vue的时候经常遇到这个问题,并且百思不得其解。这种情况下建议大家可以花点时间看看源码实现,深入了解vue响应式原理的实现。
可详见源码_init方法开始。
对于数据劫持关键方法是observe、Observer 简化后的方法如下(去掉了其他无关逻辑)
源码部分
function observe (value) {
// 只监听对象
if (!isObject(value)) {
return
}
return new Observer(value);
}
function Observer(value) {
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
};
// 关键部分
function defineReactive(obj, key, val) {
val = obj[key]
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = val
if (childOb) {
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter(newVal) {
var value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = observe(newVal)
// 此处省略了通知视图更新的方法
}
})
}
大致流程如下,初始化遍历data,如果是对象则继续watch,如果是数组,数组中存的是对象,则继续watch。当监听到数据的变化时候,会尝试重新建立observer。
测试对比案例
// 初始化tableData = []
api.then((res)=>{
//方式一
this.tableData = res.data
this.tableData.forEach((item)=>{
item.playing = false
})
// 以上这么做,修改playing字段的值时,发现视图没有跟着变化
// 然后调整为以下格式后,视图会跟着字段值变化
// 方式二
this.tableData = res.data.map((item)=>{
return {
...item,
playing:false
}
})
})
第一种写法:第一种写法是直接往上加属性,因为对象是引用类型,此时不会触发tableData的变化,从而无法触发视图层的变化。
但是第二种写法:当tableData第一次赋值时,会执行observe(val),监听新的对象,所以是有对playing属性进行watch,当playing变化时是会通知视图进行更新
关于数组项的监听
对于数组的某一项监听和数据变化需要使用$set 或者自己手动变更整个数组的方式去实现,见下例
arr = [1,2,3,4]
arr[0] = 99999
// 这种情况是监测不到变化, 因为数组没有去监听单项的变化。(原因估计得问尤雨溪为什么这么处理了,也许是基于性能考虑)
可以用下边的方式处理
this.$set(arr,[0],9999)
或 arr = [9999,2,3,4]
意外情况
data(){
return {
arr:[1,2,3,4],
age:18
}
}
this.arr[0] = 99999
this.age = 19
// 这种情况下,视图是可以响应到arr的变化的
// 因为age的变化触发视图的更新。视图更新时会去获取arr,发现其有变化,并调整视图
vue3.0已经解决了新增属性无法监测到的问题(基于proxy可以获取到对象属性的添加)