在使用vue进行代码开发时,经常会遇到需要给一个响应式对象增加属性的情况。比如我们已经存在一个响应式的属性myInfo,现在我们需要给它增加一个age属性,我们希望这个操作能通知到观测了myInfo的所有watcher(触发到视图的更新),并且当age的值发生改变时,也能通知到订阅了myInfo的所有watcher(触发到视图的更新)。
const vm = new Vue(
data: {
myInfo: {
firstName: 'x',
lastName: 'my'
}
}
);
刚开始我们可能会这样写this.myInfo.age = 24。属性是增加上去了,但一会之后你就会发现不对劲,为啥这个新增的属性不是响应式的。这时候赶紧掏出vue官方文档查一下,哦,这种时候应该使用$set去添加属性,然后赶紧去试了一下this.$set(this.myInfo, 'age', 24),果真可以了。这个时候结束了吗?没有!文章标题是$set的实现方式,那么文章才刚刚开始呢。
首先我们回到vue的官方文档看一下,在API里实例的方法中找$set方法,如图:
这个方法接收三个参数:
第一个是target,可以是数组或者对象。是准备添加属性的对象或者设置值的数组
第二个是propertyName/index。准备添加的属性的属性名或数字的索引
第三个是value。准备添加的属性的值或者数组索引的值
还提到了$set是Vue.set的别名,那我们继续去看一下Vue.set是个啥。然后在API的全局API找到了Vue.set方法,参数和$set是一样的,用法这里多了一段描述:
向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如
this.myObject.newProperty = 'hi')
还有一个注意:
注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
至此,官方文档给出的信息就结束了。这边总结一下几个关键信息:
- vm.$set是Vue.set的别名,也就是它们底层是相同的方法。
- $set是用来给响应式的对象添加属性
- 新增的属性也需要是响应式的
- 增加属性这个操作会触发视图更新
- 不能用$set方法给Vue实例和根数据对象添加属性
那$set方法是怎么做到的呢,剩下的就要开始看源码了,我看的源码是直接从github拉下来的v2.6版本。找到了定义vm.$set和Vue.set的地方,发现他们确实是引用的同一个方法,来自../observer/index的set方法。
这边推荐一下HcySunYang大佬Vue技术揭秘的附录,Vue构造函数原型、全局API等这边都有整理,看源码前想对vue构造有个大致的了解可以看一看。
// src/core/instance/state.js部分代码
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '../observer/index'
export function stateMixin (Vue: Class<Component>) {
Vue.prototype.$set = set
}
// src/core/global-api/index.js部分代码
import { set, del } from '../observer/index'
export function initGlobalAPI (Vue: GlobalAPI) {
Vue.set = set
}
接下来是找一下他们引用的set方法:
// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target需要为对象或数组
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target)) // isUndef用来判断一个值是否为null或undefined,isPrimitive用来判断一个值是否为原始类型值(string,number,boolean,symbol)
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 处理target为数组的情况
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key) // 需要先设置数组的长度,因为如果设置的索引大于数组的长度,splice会不生效
target.splice(key, 1, val) // splice是会触发响应的,所以这边直接用splice替换/设置指定位置的值
return val
}
// 处理对象的属性已经存在的情况
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// 不能给vue实例增加属性,避免覆盖vue本身的方法。
// 不能给根数据对象添加属性,首先根数据对象不是响应式的,给根数据对象添加属性不会触发视图更新。
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
}
上面代码里面已经注释了一些其他情况的分析,这边我们重点分析下面这段代码:
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
首先会判断target对象的__ob__属性是否存在。那这个__ob__属性是个啥?当vue对数据进行观测时会给这个数据增加一个不可枚举的__ob__属性,值里面包含了三个属性value,dep,vmCount三个属性。下图是数据data进行数据观测之后的数据结构的变化。
关于
__ob__属性的dep是如何收集依赖的可以去看一下src/core/observer/index.js文件里的defineReactive方法,这边就不多进行解释了。
const data = {
a: 1
};
const data = {
a: 1,
__ob__: { // __ob__是不可枚举属性
value: data, // 这边的value是指向data数据本身的
dep: dep实例对象, // new Dep(),收集了观测data对象的所有watcher
vmCount: 0
}
}
如果target对象不存在__ob__属性就说明target对象并不是一个响应式的数据,所以我们只需要对属性进行修改,然后返回值就可以了。
如果target是一个响应式的数据,那我们需要用defineReactive方法去给target增加一个响应式的属性,然后ob.dep.notify()通知所有观测了data数据的watcher,触发视图更新。
defineReactive方法是vue实现数据观测的主要方法。主要是将数据对象的数据属性转换为访问器属性,从而来收集和触发依赖。源码在src/core/observer/index.js文件里的,这边就不多进行解释了。
至此$set的实现方式就结束了,$delete的实现方式也是差不多的,感兴趣的同学可以自己去看看源码。