在 Vue 中,受限于 defineProperty 的原因,以下情况是无法触发视图更新的:
<template>
<div>
<div>{{obj}}</div>
<ul>
<li v-for="(item, idx) in arr" :key="idx">{{item}}</li>
</ul>
<button @click="add">add</button>
<button @click="change1">change1</button>
<button @click="change2">change2</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
a: 1,
b: 2
},
arr: [1, {x: 1, y: 2}]
}
},
methods: {
add() {
this.obj.c = 3; // 无法触发视图更新
this.arr[2] = 3; // 无法触发视图更新
},
change1() {
this.arr[0] = 0; // 无法触发视图更新
},
change2() {
this.arr[1].x = 0; // 触发视图更新
}
}
}
</script>
- 当给一个对象或数组新增元素时,无法触发视图更新
- 当改变数组的值时
- 如果这个值是基础类型,无法触发视图更新
- 如果这个值是对象类型,那么改变这个对象的属性值是可以触发视图更新的
- 如果想要更新视图,只能调用
Vue提供的Vue.set方法来间接实现。
1. 依赖收集
Vue.set 实际上是基于 __ob__.dep 的基础来实现派发更新的。
在之前的章节中我们已经了解了响应式对象的概念:
- 对象的属性对应着一个
dep实例,保存在defineReactive的闭包中 - 对象本身其实对应着一个
dep实例, 保存在obj.__ob__.dep中,它的作用就是为了实现Vue.set
比如如下对象:
{
a: 1,
b: [2, 3, 4],
c: {
d: 5
}
}
经过观测以后:
{
__ob__, // Observer类的实例,里面保存着Dep实例 __ob__.dep => dep(uid:0)
a: 1, // 在defineReactive闭包里存在dep(uid:1)
b: [2, 3, 4], // 在defineReactive闭包里存在着dep(uid:2),还有b.__ob__.dep => dep(uid:3)
c: { // 在defineReactive闭包里存在着dep(uid:4)
__ob__, // Observer类的实例,里面保存着Dep实例__ob__.dep => dep(uid:5)
d: 5 // 在闭包里存在着dep(uid:6)
}
}
当触发对象的 getter 时,会针对 __ob__.dep 进行相关的依赖收集,将 watcher 添加到 subs 中
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// ...
})
}
在 getter 过程中判断了 childOb,并调用了 childOb.dep.depend() 收集了依赖。其中 childOb.dep 就是我们刚刚提到的对象本身对应的 dep 对象,即 __ob__.dep。
如果 value 是个数组,那么就通过 dependArray 处理数组中对象的依赖收集:
// src/core/observer/index.js
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
Vue.set 中会通过 ob.dep.notify() 通知 watcher,那么这样就能实现让添加新的属性到对象也可以检测到变化。
下面就来看看 Vue.set 的源码
2. Vue.set
定义了一个全局 API Vue.set 方法,它在 src/core/global-api/index.js 中初始化:
Vue.set = set
这个 set 方法的定义在 src/core/observer/index.js 中:
// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
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: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
-
set方法接收 3 个参数:target可能是数组或者是普通对象key代表的是数组的下标或者是对象的键值val代表添加的值
-
首先判断如果
target是数组且key是一个合法的下标,则之前通过splice去添加进数组然后返回,这里的splice其实已经不仅仅是原生数组的splice了,稍后再介绍这部分逻辑。 -
接着又判断
key已经存在于target中,则直接赋值返回,因为这样的变化是可以观测到的。 -
接着再获取到
target.__ob__并赋值给 ob,之前分析过它是在Observer的构造函数执行的时候初始化的,表示Observer的一个实例,如果它不存在,则说明target不是一个响应式的对象,则直接赋值并返回。 -
然后通过
defineReactive(ob.value, key, val)把新添加的属性变成响应式对象。 -
最后再通过
ob.dep.notify()手动的触发依赖通知。
2.1 数组
我们刚才也分析到,对于 Vue.set 的实现,当 target 是数组的时候,也是通过 target.splice(key, 1, val) 来添加的,那么这里的 splice 到底有什么黑魔法,能让添加的对象变成响应式的呢。
其实之前我们也分析过,在通过 observe 方法去观察对象的时候会实例化 Observer,在它的构造函数中是专门对数组做了处理,它的定义在 src/core/observer/index.js 中。
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// ...
}
}
}
这里我们只需要关注 value 是 Array 的情况,首先获取 augment,这里的 hasProto 实际上就是判断对象中是否存在 __proto__,如果存在则 augment 指向 protoAugment, 否则指向 copyAugment,来看一下这两个函数的定义:
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
protoAugment 方法是直接把 target.__proto__ 原型直接修改为 src,而 copyAugment 方法是遍历 keys,通过 def,也就是 Object.defineProperty 去定义它自身的属性值。
对于大部分现代浏览器都会走到 protoAugment,那么它实际上就把 value 的原型指向了 arrayMethods,arrayMethods 的定义在 src/core/observer/array.js 中:
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
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)
// notify change
ob.dep.notify()
return result
})
})
可以看到,arrayMethods 首先继承了 Array,然后对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写。重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知。
3. Vue.del
其实对于对象属性的删除也会用同样的问题,Vue 同样提供了 Vue.del 的全局 API
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
它的实现和 Vue.set 大同小异,甚至还要更简单一些,这里就不去分析了。