# 深入了解Vue响应式特点
# 前言
在我们日常使用Vue进行开发的时候是否有注意到, 我们在使用对象或者数组的时候有些情况并没有触发响应式更新视图, 如
<div>{{ a }}</div>
let vm = new Vue({
data() {
return {
name: '123',
},
}
})
vm.a = '456' // 并不会触发视图更新
在阅读Vue源码之后, 才慢慢摸清楚了Vue2里面响应式的特点
# Vue2中对象的响应式处理
我们都知道, Vue2中是通过Object.defineProperty
进行的数据劫持, 实现响应式逻辑, 那么我们现在通过写一个仿源码的demo来理解Vue中对象的响应式处理.
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
console.log('视图更新了')
}
},
})
}
let data = {
name: 'chenying',
hobbies: {
name: 'vue',
},
}
observer(data)
data.name = 'cy' // 不会触发视图更新
data.hobbies.name = 'react' // 会触发视图更新
在demo中我们可以发现, 在Vue2的源码中是遍历data
里面的属性,通过Object.defineProperty
给每个属性添加响应式逻辑达到响应式的效果。
这就很好解释为什么我们给data
中增加不存在的属性不会触发视图更新
let vm = new Vue({
data() {
name: 'chenying',
}
})
vm.a = 123 // 不会触发视图更新
因为对于data
源码是通过遍历处理的, 对于初始化的时候data
中没有的值自然就没有响应式的逻辑了
我们在完善下代码
function defineReactive(obj, key, value) {
observer(value) // 新
Object.defineProperty(obj, key, {
get() {
return value
},
set(newValue) {
if (value !== newValue) {
observer(newValue) // 新
value = newValue
console.log('视图更新了')
}
},
})
}
这样的话如果新赋值的值是对象的话, 也是具有响应式能力的
# Vue2中数组的响应式处理
看完了对象的响应式处理, 我们再看看Vue2源码中对数组的响应式处理
首先我们需要将observer()
中进行修改, 支持处理数组类型
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
if (Array.isArray(obj)) { //如果是数组的话, 遍历挂载更新视图方法
ArrayObserver(obj)
} else { // 是对象
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
我们来进一步完善代码
function ArrayObserver(obj) {
for (let i = 0; i < obj.length; i++) {
const item = obj[i];
// 如果是普通值 就不监控了
observer(item); // 如果是对象会被 defineReactive
}
}
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
if (Array.isArray(obj)) { //如果是数组的话, 遍历挂载更新视图方法
ArrayObserver(obj)
} else { // 是对象
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
我们可以发现, Vue2源码中对数组的处理是遍历数组, 然后调用observer
方法一次进行响应式处理,这也意味着如果数组的某一项不是对象的话是不具备响应式能力的
let vm = new Vue({
data() {
array: [1, 2, {name: 'node'}],
}
})
vm.array[0] = 123 // 不会触发视图更新
vm.array[2].name = 'vue' // 会触发视图更新
最后一点, Vue2中如果调用push
, unshift
, splice
等方法对数组进行操作修改数组的长度的时候是会触发视图更新的
let arrayProto = Array.prototype // 拿到原来数组的原型缓存起来
let proto = Object.create(arrayProto);
['push', 'unshift', 'splice', 'reverse', 'sort', 'shift', 'pop'].forEach(method =>{
proto[method] = function(...args) { //重写数组的方法
let inserted; // 默认没有插入新的数据
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice': // 数组的splice 只有传递三个参数 才有追加效果
inserted = args.slice(2);
default:
break;
}
console.log('视图更新');
ArrayObserver(inserted) // 对数组新传入的数据进行响应式绑定
arrayProto[method].call(this, ...args) // 执行缓存的原数组的方法
}
})
function observer(obj) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, proto); // 新 - 实现一个对数组的方法进行重写
ArrayObserver(obj)
} else {
for (let key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
从代码中我们可以发现, 实现的逻辑是缓存原来数组原型上的方法, 当我们调用数组的push
等方法的时候实际上是调用proto
上面重写了的方法(因为Object.setPrototypeOf(obj, proto)
修改了原型链)
然后重写了的数组方法中增加响应式的逻辑, 以及对数组新加入的元素通过observer
方法一次进行响应式处理
# 总结
通过仿源码的一个小demo我们对Vue2的响应式特点有了一些了解,大致如下:
1、使用对象的时候 必须先声明属性 ,这个属性才是响应式的
2、增加不存在的属性 不能更新视图
3、默认会递归增加 getter和setter
4、数组里套对象 对象是支持响应式变化的,如果是常量则没有效果
5、修改数组索引和长度 是不会导致视图更新的
6、如果新增的数据 vue中也会帮你监控(对象类型)