本节简单看一下 Vue.set() 以及数组的方法
通过前面几节简述我们已经了解响应式对象以及依赖收集、派发通知的过程,但是对于一些特殊的情况我们还是需要再去关注一下的。
首先是关于直接为一个对象添加属性,为什么不会触发页面重新渲染?
export default {
data() {
return {
msg: { a: 1 }
}
},
mounted() {
this.msg.b = 2;
}
}
例如上述代码,我们已经在组件定义了一个数据对象 msg: {a:1} ,在 mounted() 函数内为其新添加属性,为什么不会触发页面重新渲染呢?
1、首先在初始化时会将数据对象转为响应式数据,在此过程中会为每个对象新增 __ob__ 属性且该属性持有一个 dep 实例,还会遍历对象的键 key 让其也持有一个 dep 实例,以进行依赖收集。当数据对象的键 key 对应的值发生变化时,会派发通知。
2、由上述代码可知,我们是为 msg 对象新增一个属性并赋值。由于在初始化时并没有这个键,所以没有进行依赖收集,所以此次改变也不会触发依赖更新。
有人会有疑问,说对象 msg.__ob__ 属性也持有一个 dep 实例并且初始化时也进行了依赖收集会什么不会触发重新渲染呢?呢是因为从源码角度看,派发通知是从键 key 的角度触发的,如果初始化时该 key 没有,呢么后续确实不会触发依赖通知。
官方给出使用 Vue.set(this.msg, b, 2); 才会触发页面重新渲染。接下来我们一起看看该方法式如何实现的。如下:
function set(target, key, val) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = target.__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Vue.set(target,key,val) 函数的实现还是比较简单的。
- 若
target为数组并且key为合法索引,则通过target.splice()函数直接修改对应位置的值并返回val。 - 若
key是target对象的自身属性并非原型属性,则赋值target[key] = val; - 获取到对象的重要属性
ob = target.__ob__该属性用来区分对象是否为响应式对象。 - 若
ob不存在,则target为非响应式对象,则赋值target[key] = val; - 最后通过
defineReactive()函数将key、val添加到对象上并设置为响应式数据。 - 再手动触发通知
ob.dep.notify();触发页面重新渲染。因为该ob实例也持有dep实例并进行了依赖收集,所以可以手动进行派发通知。
其次,很多时候我们直接新增/删除数组项也可以触发重新渲染,这又是怎么实现的呢?
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];
methodsToPatch.forEach(function (method) {
// 缓存原方法
const original = arrayProto[method]
// 重新定义该数组的方法
def(arrayMethods, method, function mutator (...args) {
// 先执行原方法获取结果
const result = original.apply(this, args)
// 获取当前数组的响应式标志属性 __ob__
const ob = this.__ob__
// 缓存参数
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 尝试将新增的参数转为响应式数据
if (inserted) ob.observeArray(inserted)
// 手动通知数据变化
ob.dep.notify()
return result
})
})
def (obj, key, val, enumerable = false) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable, // 默认不能枚举
writable: true,
configurable: true
})
}
由上述代码可知在 Vue 框架中,重新定义了可以修改数组长度、数组顺序的七个方法。该实现也是相对比较简单一些,下面重点看一下!
先缓存数组原方法,然后通过 Object.defineProperty() 函数重新在数组上定义该函数。在该函数内部:
- 首先执行数组原方法获取结果
result - 再获取数组的响应式标志属性
ob = this.__ob__ - 再获取数组新增的参数,再尝试将参数转为响应式数据
- 手动触发通知
ob.dep.notify()进行重新渲染 - 最后返回结果
result