defineReactive定义响应式的方法#
defineReactive函数从名字上看应该是一个定义响应式属性的函数。
下面看它的代码,在core/observer/index.js文件中。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
}其实在前面调用initRender函数中定义vm.$attrs和vm.$listeners属性的时候就已经使用过defineReactive函数。
先看这个函数的参数,有五个参数。前面三个参数分别是定义的对象obj以及它的key值和val值,这些参数是定义响应式属性时会用到的属性。其中第一个参数obj和第二个参数key是一定要传入的,第三个参数val可以在函数中通过val[key]来获取。
第四个参数customSetter,是一个自定义的setting函数。在非生产环境中,可以设置一个用来提醒或者报错的函数。
第五个参数shallow是个布尔值,表示是否开启深度监听,当它的值为false时开启深度监听,这个参数一般是不会传入的,也就是说它的值默认是undefined,然后在使用它的时候可以取反,即!shallow的值为true,这样就会开启深度监听。
下面看函数的代码。
const dep = new Dep(); // ! 实例化一个订阅器
const property = Object.getOwnPropertyDescriptor(obj, key); // ! 获取对象的 key 属性描述符
if (property && property.configurable === false) {
return;
}
// ...先看第一行代码,实例化一个订阅器对象dep,用来收集观察者依赖。然后尝试获取对象obj中key的属性描述符。
什么是属性描述符呢?就是对象中key的属性描述,它也是一个对象。如下面的代码。
const obj = {
a: 1,
};
Object.getOwnPropertyDescriptor(obj, "a");
// { value: 1, writable: true, enumerable: true, configurable: true }然后判断获取到属性描述符且它的configurable的值是否为false,即确认一下这个key的属性是否可以配置(能否被改变),如果configurable的值为false,也就是说key是不可配置的,那么直接返回。
再看下面的代码。
// cater for pre-defined getter/setters
// ! 缓存对象原先定义的 getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}属性描述符既然是key的属性描述,那么它里面可能会有getter和setter属性,尝试去获取它们的值。
然后是一个条件语句,判断是否只能获取setter或者没有getter,并且函数的参数只有两个,即只传入前面的obj和key这两个参数,当满足条件时,获取val的值。
为什么这里要获取val的值呢?因为在walk方法中调用defineReactive时只传了前面两个参数,并没有传第三个参数val,所以应该需要获取val的值,这也满足条件语句的第二个条件。
然后再看第一个条件。
!getter || setter;需要满足没有getter或者有setter这两个条件中的一个才行。这个要怎么理解呢?
先看下条件语句之后的这行代码。
let childOb = !shallow && observe(val); // ! 当 val 不为 undefined 深度监听如果不满足上面条件语句中的两个条件的话,在深度监听的时候,val的值就是undefined,observe(val)就不会被有效的执行,所以也就不会生成实例对象childOb,也就是这时候深度监听是失败的。
也就是说如果不满足上面的条件,就获取不到val,也就不会对val执行深度监听。
看下满足条件!getter || setter的三种情况:
- 有
getter也有setting(假真):在属性被observe监听后会满足这种情况。 - 没有
getter也没有setting(真假):在属性变成响应式数据前会满足这种情况。 - 没有
getter有setting(真真):这种情况比较少见。
再看下不满条件的情况也就是(假假),即有getting而没有setting,我们可以举个例子,看下面代码。
// const data = {a: { b: 1 }} 对比一般的字面量声明对象
const data = {};
// 使用 Object.defineProperty 只定义 getter
Object.defineProperty(data, "a", {
get() {
return { b: 1 };
},
});
const vm = new Vue({
el: "#app",
data,
watch: {
"a.b": function () {
console.log("监听到a.b");
},
},
});
window.vm = vm;在控制台上输入vm._data,输出下面结果
{
a: {
b: 1
}
__ob__: {
value: {/* */},
dep: {/* */},
vmCount: someNumber
}
}在data选项里面的对象a中并没有__ob__这个属性,说明数据a并没有被深度监听,此时如果修改对象a里面的b属性,是不会被watch选项里面的监听函数监听到的。
但是我们在字面量定义data选项的数据时,对象a里面是有__ob__属性的,因为深度监听是默认开启的。
const data = { a: { b: 1 } };小结,一般情况下val都是会去尝试获取值并且对它执行深度监听。只有在特殊场景下才不会去获取值,具体是什么特特殊场景,后面遇到再讲。
getter#
接下来看下defineReactive函数中是怎么定义getter的。
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 // ! 返回值
},
set: function reactiveSetter(newVal) { /* ... */ }
}首先声明常量value存储获取到val的值,同时也作为getter的返回值。
怎么获取val的值?判断该数据原是否已经定义过getter,如果已经定义过就直接调用原先的getter进行求值,否则返回上面获取的val值。
为什么要调用原先定义的getter来返回值呢?因为不能去重复定义getter,这样会把以前的定义的getter覆盖掉。
然后判断Dep.target是否有值(观察者),如果有值的话,使用订阅器对象dep收集观察者依赖。如果有childOb,也就是说当前的value应该是一个对象,同样让childOb.dep收集观察者依赖,这个childOb就是对象value里面的__ob__属性。
注意,这两个dep收集到的依赖是一样的,但是它们触发依赖的时机不一样,我们在修改value的值后,会执行它的setter,会触发obj中的ob的依赖执行。
但是如果value也是一个对象类型时,当为这个对象添加或者删除属性时,这时是不会触发obj的ob的依赖执行的。因为新增的属性或者删除的属性中里面并没有setter,但是value确实被修改了。这时会触发value里面的ob(也就是childOB)中的依赖执行。
示例代码
user: {
name: 'hale',
info: {
age: 18,
score: 99,
__ob__: {/* */} // childOb
},
__ob__: {/* */} // ob
}
当我们修改user数据下面的name时,比如把hale修改为bill,这时会触发ob中依赖的更新。或者修改info的值为null时,也会触发ob中依赖的更新。但是当我们修改info里面的值时,比如为它添加一个新的属性gender,它的值为man,这时候就不会触发ob中依赖的更新,而是会触发childOb中依赖的更新。另外,ob和childOB里面的依赖是一样的。
有childOb说明是深度监听,说明监听对象obj里面的value也是一个对象类型。这时再判断value是不是一个数组,如果是数组,调用dependArray手动收集依赖,因为数组里面的索引值是不会自动收集依赖的。后面详细说明它是如何收集依赖的。
总结,getter函数有两个作用,一个是返回正确的值,另一个就是收集依赖。
setter#
下面看下setter的定义
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val; // ! 获取值 (旧值)
/* eslint-disable no-self-compare */
// ! NaN === NaN false
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal); // ! 执行原来的 setter, 设置新值
} else {
val = newVal; // ! 设置新值
}
childOb = !shallow && observe(newVal); // ! 深度监听新值(对象或者数组)
dep.notify(); // ! 通知观察者, 触发依赖
}首先声明常量value存储获取到val的值。怎么获取val的值?判断属性原先是否已经定义过getter,如果已经定义过就直接调用原先的getter进行求值,否则返回上面获取的val值。
注意这里获取的值是修改前的值,也就是旧值。而修改后的值就是新值是通过参数newVal传进来的。然后比较新值和旧值是否全等,如果全等,直接返回,不需要进行下面的操作。
这里有个判断条件非常古怪,即新值的本身不等于新增,旧值的本身不等于旧值。
newVal !== newVal && value !== value;请问可能存在这种奇怪的现象吗?答案是肯定的。JavaScript 中就存在一个特殊的值NaN,它不等于它自身,也就是说如果新值或者旧值是NaN的时候,也是直接返回。
继续看下面代码。
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}customSetter是defineReactive函数的第四个参数,是一个自定义的setter函数,如果这个参数有值的话且在非生产环境时,会在这里执行,这个函数都是一个警告或者提示的函数。
比如在initRender函数初始化实例对象的$attrs和$listeners属性时传入的函数,看它的代码。
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
},这个函数是在不是更新子组件时修改$attrs属性的时候,会提示$attrs是可读的。
继续看剩下的代码。
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal); // ! 执行原来的 setter, 设置新值
} else {
val = newVal; // ! 设置新值
}
childOb = !shallow && observe(newVal); // ! 深度监听新值(对象或者数组)
dep.notify(); // ! 触发依赖,通知观察者判断原来的属性描述符中是否只有getter而没有setter,如果是的话,直接返回。因为这个属性既然原来只设置了getter而没有设置setter,这里也不用画蛇添足的去设置setter。
判断原来是否有setter,如果有的话,调用原来的setter并传入新值。如果没有的话,直接把新值赋值给旧值。这里和getter中原因是一样的,原先的属性已经定义过setter,就不需要再重新定义setter,而是直接调用它。
然后深度监听这个设置的新值。因为设置的新值可能也是一个对象类型的值,这时需要进行深度监听。
setter函数在最后会触发依赖,通知观察者。
dep.notify(); // ! 触发依赖,通知观察者小结,setter函数有两个作用,一个是正确修改值,另一个就是触发依赖。
childOb#
经过Object.defineProperty定义过的数据,在修改数据的值时,会触发其中的setting的执行,触发依赖的执行,更新视图。这里触发的依赖是和这个数据平级的__ob__属性中dep收集的依赖。
如果这个数据是一个对象类型的值,当它在新增或者删除属性时,也相对于改变了值,但是它改变的是数据里面的值,但是也需要去触发依赖,更新视图的。不过实际上它并不会触发和属性平级的__ob__属性里面的dep收集的依赖执行的。
这时候就需要使用到childOb,使用它的dep收集的依赖来触发依赖。childOb就是在数据里面的__ob__属性。在我们使用Vue.set/Vue.delete或者vm.$set/vm.$delete更新数据时,执行的依赖都是childOb中的依赖。
比如现在有一个data选项:
const data = {
a: { b: 1 },
c: 2,
};data选项是响应式的。
const data = {
a: {
b: 1,
__ob__: { value, dep, vmCount }, // childOb
},
c: 2,
__ob__: { value, dep, vmCount }, // ob
};在我们修改对象a的值,必须把它变成a:1,这时会触发ob1中dep收集的依赖,即执行a.__ob__.dep.notify代码。修改c的值也是一样的。
但是如果我们修改的是a里面的值,比如新增一个属性,比如给对象a新值属性bb,它的值为 2。
Vue.set(a, "bb", 2);或者直接修改b的值,这时候还是会触发对象a依赖执行的,不过现在触发的不是ob1下面的dep收集的依赖,而是ob里面的dep收集的依赖,即执行a.b.__ob__.dep.notify()代码,也就是childOb里面的代码。
在我们使用Vue.delete或者vm.$delete修改值也是一样的情况。
至于如何执行childOb的代码,触发依赖,在我们学习到set和delete函数时会详细分析。