先看一段vue官网中的描述:
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject,'b',2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
vm.$set
src/core/instance/state.ts
Vue.prototype.$set = set
src/core/global-api/index.ts
Vue.set = set
src/core/observer/index.ts
export function set(
target: any[] | Record<string, any>,
key: any,
val: any
): any {
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`
)
}
const ob = (target as any).__ob__
if (isArray(target) && isValidArrayIndex(key)) {
// ...数组暂时不看
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
if ((target as any)._isVue || (ob && ob.vmCount)) {
__DEV__ &&
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, undefined, ob.shallow, ob.mock)
if (__DEV__) {
// ...
} else {
ob.dep.notify()
}
return val
}
Vue.set和vm.$set调用的都是同一个方法;
首先traget不能是null,undefined,基本数据类型,否则抛出警告;
1. key属性已经是响应式
如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true,所以如果key不存在于target中,并且不存在于Object原型链中,说明这个key属性已经是响应式了,可以直接观察到了,直接赋值就行了target[key] = val;
2. 对象不能是Vue 实例,或者 Vue 实例的根数据对象
vue官网描述:
注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
if ((target as any)._isVue || (ob && ob.vmCount)) ,抛出警告;
3. 如果对象不是响应式
const ob = (target as any).__ob__,if (!ob) ,如果target中不存在_ob_属性,说明target不是响应式对象,直接赋值,就不用做特殊处理了
export class Observer {
constructor(public value: any, public shallow = false, public mock = false) {
def(value, '__ob__', this)
}
可以看到,对象做观测时,会将Observer实例赋值给对象的_ob_属性;
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock),把新添加的属性key变为响应式,在Observer构造器函数中,第一个参数public value,obj.value拿到的就是target,如果val为对象,val内部的属性也会被变成响应式。
4. 手动通知更新ob.dep.notify()
先看gettter中的一段逻辑:
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep()
let childOb = !shallow && observe(val, false, mock)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
if (__DEV__) {
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
}
}
},
set: function reactiveSetter(newVal) {
})
return dep
}
childOb.dep.depend()收集了依赖,这个逻辑专门为vm.$set准备的,所以在set函数中手动执行ob.dep.notify(),这样新添加属性也能触发依赖watcher,例如:
<body>
<div id="app">
<div ref="name">{{obj}}</div>
<button @click="change">change</button>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
return {
obj: {
foo: 'abc'
}
}
},
methods: {
change() {
this.$set(this.obj, 'bar', 123)
}
}
})
</script>
</body>
在这个例子中,childOb返回的是obj的Observer实例ob,在读取obj的get函数中childOb.dep.depend()添加了obj的依赖渲染watcher,在set函数中,给obj添加新属性bar,执行defineReactive(obj, 'bar', '123'),将bar属性转换成响应式,同时手动触发更新,ob.dep.notify(),收集的obj的依赖watcher会被触发
vm.$delete
export function del(target: any[] | object, key: any) {
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(
`Cannot delete reactive property on undefined, null, or primitive value: ${target}`
)
}
if (isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target as any).__ob__
if ((target as any)._isVue || (ob && ob.vmCount)) {
__DEV__ &&
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if (isReadonly(target)) {
__DEV__ &&
warn(`Delete operation on key "${key}" failed: target is readonly.`)
return
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
if (__DEV__) {
} else {
ob.dep.notify()
}
}
vm,$delete与vm.$set逻辑类似, delete target[key]删除属性, if (!ob) { return },在手动触发更新前,先判断target是不是响应式,如果不是直接返回,什么都不用做,是响应式,再去手动触发更新 ob.dep.notify()