本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本文基于
Vue 3.2.30版本源码进行分析
为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,大部分伪代码会采取js的形式展示,而不是原来的ts代码
本文不讲解shallowRef、shallowReactive、shallowReadonly中使用集合类(Map、Set)的相关内容
本文内容
- ref数据类型初始化、依赖收集、派发更新以及常用方法等内容结合源码的分析
- readonly数据类型应用场景、初始化、依赖收集、派发更新等内容结合源码的分析
- shallowRef、shallowReactive、shallowReadonly数据依赖收集、派发更新等内容结合源码的分析
文章部分内容摘录于Vue3官方文档
Ref
1. 初始化
- 初始化
dep,后面用来存储effects,进行依赖收集和派发更新 __v_isRef=true- 如果传递的
value是原始值,那么this._rawValue=原始值,this._value=原始值
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
}
function toRaw(observed) {
const raw = observed && observed["__v_raw" /* RAW */];
return raw ? toRaw(raw) : observed;
}
const toReactive = (value) => isObject(value) ? reactive(value) : value;
function reactive(target) {
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!isObject(target)) { return target; }
}
- 如果传递的
value是响应式对象,那么this._rawValue=原始对象,this._value=响应式对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!isObject(target)) { return target; }
// target is already a Proxy, return it.
if (target["__v_raw" /* RAW */] &&
!(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
return target;
}
}
- 如果传递的
value是对象,那么this._rawValue=原始对象,this._value=响应式对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// ...target是原始值/响应式对象的处理
// target是原始对象的处理
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
2. 依赖收集
从之前的文章Vue3源码-响应式系统-依赖收集和派发更新流程浅析可以知道,当effect触发Ref.value数据时,会进行依赖收集
从下面的代码块可以知道,当触发依赖收集时,Ref.dep会收集目前的effect
get value() {
trackRefValue(this);
return this._value;
}
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
trackEffects(ref.dep || (ref.dep = createDep());
}
}
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
3. 派发更新
从之前的文章Vue3源码-响应式系统-依赖收集和派发更新流程浅析可以知道,当effect重新执行时,即触发effect.run()时,会触发Ref.value的get获取最新的数据,重新触发依赖收集,并且返回effect.run()的值
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
triggerEffects(ref.dep);
}
}
function triggerEffects(dep, debuggerEventExtraInfo) {
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
}
4. Ref类型数据结构应用总结
- 追踪原始值的变化,使用
.value作为一种特殊的key,类似于Proxy的响应式是拦截get和set请求,然后在get和set请求进行track和trigger,进行依赖收集和派发更新,Ref数据是使用.value的get和.value的set进行trackRefValue依赖收集和triggerRefValue派发更新 - 提供给响应式对象
Reactive进行单个key的响应式转化,通过.value封装了响应式对象Reactive某个key的get和set请求细节,对于一些应用场景,比如setup()返回整个响应式对象Reactive时,想要布局直接写count,而不是写reactiveObject.count时,Ref提供了给响应式对象Reactive一种不失去响应式的解构功能
上面已经分析了第一点相关应用,下面流程将讲解第二点
5. Ref和Reactive的相互转化
(1) toRef
应用示例
setup(props) {
const obj = reactive({ count: 1, temp: 2 });
const countRef = toRef(obj, count);
return {
countRef
}
}
源码分析
本质是模仿原始值的封装方式,将reactive(obj)的每一个key对应的数据value当作一个原始值
新建一个代理对象ObjectRefImpl,初始化时传入key+原数据Object,通过代理对象ObjectRefImpl.value的get和set方法,间接操作原数据Object,相当于代理对象ProxyObject封装了原数据Object某个key的所有操作逻辑,我们只关心代理对象ProxyObject对应的key即可
get请求:countRef初始化时已经传入key,使用countRef.value获取值,触发的是初始化传入的this._object[this._key]set请求:countRef初始化时已经传入key,使用countRef.value设置值,触发的是初始化传入的this._object[this._key]=xxxxx
function toRef(object, key, defaultValue) {
const val = object[key];
return isRef(val)
? val
: new ObjectRefImpl(object, key, defaultValue);
}
class ObjectRefImpl {
constructor(_object, _key, _defaultValue) {
this._object = _object;
this._key = _key;
this._defaultValue = _defaultValue;
this.__v_isRef = true;
}
get value() {
const val = this._object[this._key];
return val === undefined ? this._defaultValue : val;
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
(2) toRefs
应用示例
setup(props) {
const obj = reactive({ count: 1, temp: 2 });
const objRef = toRefs(obj, count);
return {
...objRef
}
}
源码分析
toRefs本质也是使用toRef进行某一个key的代理对象的创建,只不过toRefs是进行所有key的所有代理对象的创建
function toRefs(object) {
const ret = isArray(object) ? new Array(object.length) : {};
for (const key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
toRefs又新建了一个代理对象,使用key作为索引,将所有toRef创建的对象进行存储,最终形成
toRefsObject: {
[key1]: {
get value() {
// 操作reactiveObject[key1]
},
set value() {
// 操作reactiveObject[key1]
}
},
[key2]: {
get value() {
// 操作reactiveObject[key2]
},
set value() {
// 操作reactiveObject[key2]
}
},
}
(3) 使用Ref作为Reactive的一个属性,自动解包value进行更新和获取值
应用示例
const countRef = ref(0);
const obj = reactive({count: countRef});
obj.count = 2222; // 不用写value,自动更新countRef
源码分析
从下面的源码可以看出,如果使用了Ref作为其中一个属性,那么触发set操作时会自动进行判断传入的值是不是原始值,然后自动触发oldValue.value= newValue
target必须不是Array类型,为什么不是Array类型请看下面ref 在响应式对象/Array中不进行解包的分析
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
let oldValue = target[key];
if (!shallow && !isReadonly(value)) {
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
}
}
6. Ref和watch
(1) 为什么watch(Ref)是浅监听,而watch(Reactive)是深度监听呢?
- 由下面代码块可以发现,如果初始化传入一个
Ref数据,则直接获取Ref.value,此时deep=false,只有传入一个Reactive数据时,才会触发deep=true - 而
deep直接影响了getter的构建,即deep=true=>getter = () => traverse(baseGetter())
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
if (isRef(source)) {
getter = () => source.value;
forceTrigger = isShallow(source);
}
else if (isReactive(source)) {
getter = () => source;
deep = true;
} else if (isArray(source)) {
isMultiSource = true;
forceTrigger = source.some(isReactive);
getter = () => source.map(s => {
if (isRef(s)) {
return s.value;
}
// ....
});
}
if (cb && deep) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
}
(2) 如果传入一个Reactive数据,再传入deep=false会怎么样?
从上面的源码看出,就算你传入
deep=false,最终也会被强制转化deep=true
watch还支持传入function类型,即getter=()=> reactive.count,这样就能实现监听reactive.count的目的,而不是深度监听
(3) watch支持监听原始值吗?
从上面的代码块可以发现,如果想要监听原始值,我们得传入Ref数据或者使用function作为source,即
const countRef = ref(333);
watch(countRef, ()=> {
// 可以监听countRef的改变
});
countRef.value = 555;
const obj = reactive({count: 333});
watch(obj, ()=> {
// 可以监听obj.count的改变
});
watch(obj.count, ()=> {
// 无法监听obj.count的改变,因为obj.count是原始值
// 在上面doWatch源码中,getter为空,最终不会执行obj.count
// 因为无法触发obj-Proxy的get请求触发依赖收集
});
watch(()=> obj.count, ()=> {
// 可以监听obj.count的改变
// obj-Proxy的get请求触发依赖收集
});
obj.count = 4444;
7. ref 在响应式对象/Array中不进行解包
参考 vuejs/core/issues/737,
Array/Map/Set将不会解包ref数据
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包,如下面代码块所示,嵌套在一个深层响应式对象内时会自动解包
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包,如下面代码块所示,不会自动进行解包
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
readonly
1. 详细信息
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的
const original = reactive({ count: 0 });
const copy = readonly(original);
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count);
})
// 更改源属性会触发其依赖的侦听器
original.count++;
// 更改该只读副本将会失败,并会得到一个警告
copy.count++; // warning!
2. 源码分析
结合上面应用场景的代码块进行分析,请注意,上面
readonly包裹的是一个Proxy对象,而不是简简单单一个Object对象,这种特殊复杂的例子可以帮助我们更好理解整个流程
(1) 初始化
从下面的源码可以看出,const copy = readonly(original)会将target=Proxy对象传入,为original这个Proxy对象再构建一个new Proxy()对象,我们成为代理对象Child,代理对象original我们成为代理对象Parent
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 如果是非读的Proxy,直接返回,不再进行转化
if (target["__v_raw" /* RAW */] &&
!(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
return target;
}
// isReadonly=true时,第二个参数是readonlyHandlers,即new Proxy(target,readonlyHandlers)
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
(2) 依赖收集
当watchEffect触发copy.count时,由于copy是一个Proxy对象,因此会触发它的get()方法,此时isReadonly=true
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
}
}
从而触发了Reflect.get(target, key, receiver),由于copy(代理对象Child)实际上是为了target=original(代理对象Parent)建立的Proxy对象,因此Reflect.get(target, key, receiver)会触发original的get()方法,此时isReadonly=false
由于isReadonly=true数据不用track,因为它根本不能改变(包括set、delete、add),不会改变自然也不需要追踪它的变化
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
但是original(代理对象Parent)的get()方法中isReadonly=false,因此会追踪它的变化,进行依赖收集,将当前的watchEffect收集到origin这个Proxy的deps中,依赖收集完成
(3) 派发更新
原始Proxy对象触发更新
从下面的代码可以知道,当触发Proxy对象的original.count set()时,会触发派发更新,触发上面依赖收集的effect的重新执行
original这个
Proxy代理对象的isReadonly=false,可以进行更改
document.getElementById("testBtn").addEventListener("click", () => {
// 更改源属性会触发其依赖的侦听器
original.count++
});
isReadonly=true的Proxy对象触发更新
document.getElementById("testBtn1").addEventListener("click", () => {
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!
});
当触发上面的代码块copy.count++,即触发Proxy对象的copy.count set()时,由下面初始化的代码可以知道,set()方法直接拦截返回true,然后弹出警告,copy.count++被阻止执行了
const readonlyHandlers = {
get: readonlyGet, // 本质上还是createGetter()
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
},
deleteProperty(target, key) {
console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
}
};
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
const proxy = new Proxy(target, readonlyHandlers);
proxyMap.set(target, proxy);
return proxy;
}
3. 多层嵌套场景-源码分析
(1) 示例
const original = reactive({ count: { temp: 333, temp1: 455 }, count1: 4444 });
const copy = readonly(original);
watchEffect(() => {
console.info("----------------------------------------------")
// 用来做响应性追踪
console.log(copy.count.temp);
console.info("----------------------------------------------")
});
document.getElementById("testBtn2").addEventListener("click", () => {
copy.count.temp = new Date().getTime();
});
(2) 派发更新源码更新
下面图片展示的是派发更新流程,而依赖收集跟派发更新核心流程大同小异,主要流程为
readonly->reactive->对象->返回reactive(object)->返回readonly(reactive)->reactive->原始对象获取原始值->返回原始值给reactive->返回原始值给readonly,显示在界面,将不再展示依赖收集的流程 详细可以查看运行github-readonly.html后的console调试信息
shallowReadonly
1. 详细信息
readonly() 的浅层作用形式,和 readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为ref的属性不会被自动解包了
2. 源码分析
(1) 初始化
从下面的代码可以看出,本质上shallowReadonlyHandlers初始化也是复用了readonlyHandlers的set、delete,然后稍微改变了createGetter()的传值,都为true
new Proxy()的代码都是同样的逻辑,这里不再赘述,唯一区别就是new Proxy(target, handler)传入的第二个参数,这里传入的是shallowReadonlyHandlers(暂时不考虑集合类)
const shallowReadonlyHandlers = /*#__PURE__*/ extend({}, readonlyHandlers, {
get: shallowReadonlyGet
});
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
//.......
const res = Reflect.get(target, key, receiver);
if (shallow) {
// 值为 ref 的属性不会被自动解包了,因为在解包前就return了
return res;
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
}
}
(2) 代码示例
const original = reactive({ count: { temp: 3 } })
const copy = shallowReadonly(original)
effect(() => {
console.warn("===========copy effect触发===========");
console.log(copy.count.temp);
console.warn("===========copy effect触发===========");
});
document.getElementById("testBtn").addEventListener("click", () => {
copy.count = new Date().getTime();
});
document.getElementById("testBtn1").addEventListener("click", () => {
copy.count.temp = new Date().getTime();
});
(3) 依赖收集
(4) 派发更新
copy.count = new Date().getTime()会直接触发readonly的set()方法,然后直接提示错误,返回true,什么操作都不会触发copy.count.temp = new Date().getTime()会触发派发更新,因为根据上面依赖收集的分析,由于shallowReadonly的特性,copy.count是一个reactive对象,而不是一个readonly对象,因此它会收集依赖以及派发更新,流程图如下:
shallowRef
1. 详细信息
Vue 的响应性系统默认是深度的。在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪
Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新
const shallowArray = shallowRef([
/* 巨大的列表,里面包含深层的对象 */
])
// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]
// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]
2. 常见用法
triggerRef()
强制触发依赖于一个浅层 ref 的副作用,如下面代码块所示,triggerRef可以强制触发shallowRef依赖收集的watchEffect的重新执行
const shallow = shallowRef({
greet: 'Hello, world'
})
// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'
// 强制触发shallowRef依赖收集的effect的重新执行
// 触发上面的watchEffect打印 "Hello, universe"
triggerRef(shallow)
3. 源码分析
如果初始化传入
value也是Ref对象,从下面源码可以知道,只会直接返回Ref对象,不会启动new RefImpl初始化流程
function shallowRef(value) {
return createRef(value, true);
}
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
从下面代码可以知道,当__v_isShallow=true时
- 初始化
this._rawValue = value,而不会进行toRaw(value)获取原始数据,当数据发生变化时,触发set value,也只会比较根元素hasChanged(newVal, this._rawValue)是否发生了改变,只会hasChanged()=true才会触发triggerRefValue派发更新 - 初始化
this._value = value,而不会进行toReactive(value)进行对象的深度响应式转化,因此只有根元素.value是有进行get value()和set value(),只有根元素.value是响应式的,而深一层的对象是不会进行Proxy构建和方法拦截的,如const refCount = ref({count:1})- 当
__v_isShallow = false时,访问refCount.value.count时会触发refCount get value()方法和Proxy({count: 1}) get()方法,因此会触发两个响应式变量,一个是RefImpl,一个是Proxy进行依赖收集 - 当
__v_isShallow = true时,访问refCount.value.count时因为{count: 1}不是响应式的,根元素才是响应式的,因此只会触发refCount get value()方法,触发RefImpl的依赖收集
- 当
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
shallowReactive
1. 详细信息
Vue 的响应性系统默认是深度的。在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪
Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性是响应式的
state.foo++
// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false
// 不是响应式的
state.nested.bar++
2. 源码分析
(1) 依赖收集
从下面的代码可以看出,shallow=true时,会直接将整个对象返回,而shallow=false时,还会深度遍历转化reactive(res)进行深度key的Proxy转化
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) { return res }
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
shallow=false时,比如const reactiveObj = reactive({count: {temp: 3}}),由于会调用reactive(res)进行深度key的Proxy转化,因此表现为如下:
const reactiveObj = reactive({count: {temp: 3}})
effect(() => {
// 结合上面源码块
// reactiveObj.count触发reactiveObj的Proxy-createGetter()请求,获得 reactive({temp: 3}),称为代理对象Proxy-A
// reactiveObj.count.temp触发"代理对象Proxy-A".temp,即触发"代理对象Proxy-A"的Proxy-createGetter()请求,获取3
console.log(reactiveObj.count.temp);
});
shallow=true时,比如const reactiveObj1 = shallReactive({count: {temp: 3}}),由于if (shallow) { return res },因此表现为如下:
const reactiveObj1 = shallReactive({count: {temp: 3}})
effect(() => {
// reactiveObj.count触发reactiveObj的Proxy-createGetter()请求,获得 {temp: 3},称为对象B
// reactiveObj.count.temp触发"对象B".temp,不会触发任何Proxy-createGetter()请求,直接获取3
console.log(reactiveObj1.count.temp);
});
(2) 派发更新
由上面依赖收集的分析可以知道,shallReactive只会触发根元素的依赖收集,也只会转化根元素为Proxy数据
继续使用const reactiveObj = reactive({count: {temp: 3}})这个例子,由于根元素才是Proxy对象,深层的key不会进行reactive()转化,因此reactiveObj1.count.temp=xxx不会触发任何响应式的派发更新,只有reactiveObj1.count才会触发Proxy的set方法,触发响应式派发更新,触发effect的重新执行
const reactiveObj1 = shallReactive({count: {temp: 3}})
document.getElementById("testBtn").addEventListener("click", () => {
// 不会触发Proxy-set方法,因为count:{temp: xxx}不是Proxy数据
reactiveObj1.count.temp = new Date().getTime();
// 会触发Proxy-set方法,因为根元素reactiveObj1:{count}是Proxy数据
// 会触发Proxy.set(),进而触发派发更新
reactiveObj1.count = new Date().getTime();
});
结束语
ref、shallow、readonly类型数据的代码逻辑分散在各个地方,本文写在最后,仍然觉得还有很多细小地方没有进行分析,如果有读者发现文章中有部分源码情况没有覆盖,麻烦在评论区中告知,我将尽快补充完善