时间:2023-05-09 15:45:02 | 来源:网站运营
时间:2023-05-09 15:45:02 来源:网站运营
Vue3响应式系统源码解析-单测篇:注意:在我写文章的时候,可能代码已有变更。在您读文章的时候,代码更有可能变更,如有不一致且有会对源码实现或理解产生重大不一致,欢迎指出,万分感谢。
这个包会内嵌到vue的渲染器中(@vue/runtime-dom)。不过它也可以单独发布且被第三方引用(不依赖vue)。但是呢,你们也别瞎用,如果你们的渲染器是暴露给框架使用者的,它可能已经内置了一套响应机制,这跟咱们的reactivity是完全的两套,不一定兼容的(说的就是你,react-dom)。唔,单根据Readme,无法清晰的知道,它具体是怎么样的。毕竟也是alpha版。那我们还是听它的,直接撸源码吧。
关于它的api呢,大家就先看看源码或者看看types吧。注意:除了Map
,WeakMap
,Set
andWeakSet
外,内置的一些对象是不能被观测的(例如:Date
,RegExp
等)。
ref
、reactive
、computed
、effect
、lock
、operations
。其中 lock
跟 operations
很简单, lock
文件内部就是两个控制锁开关变量的方法, operations
内部就是对数据操作的类型的枚举。ref
、reactive
、computed
、effect
这四个文件,但这四个文件就没这么简单了。我花了半天,从头到尾的撸了一遍,发现每个字母我都认识;每个单词,借助google,我也都知道;基本所有的表达式,我这半吊子的TypeScript水平也都能理解。但是,当它们组成一个个函数的时候,我就有点儿懵逼了.....ref
里引了 reactive
, reactive
里又引用了 ref
,再加上函数内部一下奇奇怪怪的操作,绕两下便迷糊了。reactive
顾名思义,响应式,意味着 reactive
数据是响应式数据,从名字上就说明了它是本库的核心。那我们先来看看它有什么样的能力。test('Object', () => { const original = { foo: 1 } const observed = reactive(original) expect(observed).not.toBe(original) expect(isReactive(observed)).toBe(true) expect(isReactive(original)).toBe(false) // get expect(observed.foo).toBe(1) // has expect('foo' in observed).toBe(true) // ownKeys expect(Object.keys(observed)).toEqual(['foo'])})
看着好像没啥,就是向 reactive
传递一个对象,会返回一个新对象,两个对象类型一致,数据长得一致,但引用不同。那我们顿时就明白了,这肯定是利用了Proxy!vue@3响应式系统核心中的核心。reactive
的声明:reactive
只接受对象数据,返回的是一个 UnwrapNestedRefs
数据类型,但它到底是个啥,也不知道,以后再说。test('Array', () => { const original: any[] = [{ foo: 1 }] const observed = reactive(original) expect(observed).not.toBe(original) expect(isReactive(observed)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReactive(observed[0])).toBe(true) // get expect(observed[0].foo).toBe(1) // has expect(0 in observed).toBe(true) // ownKeys expect(Object.keys(observed)).toEqual(['0'])})
reactive
接收了一个数组(数组自然也是object),返回的新数组,不全等于原数组,但数据一致。跟单测一中的对象情况表现一致。不过这个单测没考虑嵌套的,我补充一下test('Array', () => { const original: any[] = [{ foo: 1, a: { b: { c: 1 } }, arr: [{ d: {} }] }] const observed = reactive(original) expect(observed).not.toBe(original) expect(isReactive(observed)).toBe(true) expect(isReactive(original)).toBe(false) expect(isReactive(observed[0])).toBe(true) // observed.a.b 是reactive expect(isReactive(observed[0].a.b)).toBe(true) // observed[0].arr[0].d 是reactive expect(isReactive(observed[0].arr[0].d)).toBe(true) // get expect(observed[0].foo).toBe(1) // has expect(0 in observed).toBe(true) // ownKeys expect(Object.keys(observed)).toEqual(['0'])})
说明返回的新数据,只要属性值还是个object,就依旧 isReactive
。test('observed value should proxy mutations to original (Object)', () => { const original: any = { foo: 1 } const observed = reactive(original) // set observed.bar = 1 expect(observed.bar).toBe(1) expect(original.bar).toBe(1) // delete delete observed.foo expect('foo' in observed).toBe(false) expect('foo' in original).toBe(false)})
reactive
执行后返回的响应数据,对其做任何写/删操作,都能同步地同步到原始数据。那如果反过来,直接更改原始数据呢?test('observed value should proxy mutations to original (Object)', () => { let original: any = { foo: 1 } const observed = reactive(original) // set original.bar = 1 expect(observed.bar).toBe(1) expect(original.bar).toBe(1) // delete delete original.foo expect('foo' in observed).toBe(false) expect('foo' in original).toBe(false)})
我们发现直接修改原始数据,响应数据也能获取的最新数据。test('observed value should proxy mutations to original (Array)', () => { const original: any[] = [{ foo: 1 }, { bar: 2 }] const observed = reactive(original) // set const value = { baz: 3 } const reactiveValue = reactive(value) observed[0] = value expect(observed[0]).toBe(reactiveValue) expect(original[0]).toBe(value) // delete delete observed[0] expect(observed[0]).toBeUndefined() expect(original[0]).toBeUndefined() // mutating methods observed.push(value) expect(observed[2]).toBe(reactiveValue) expect(original[2]).toBe(value)})
第六个单测证明了通过 Proxy
实现响应式数据的巨大好处之一:可以劫持数组的所有数据变更。还记得在vue@2中,需要手动set数组吗?在vue@3中,终于不用做一些奇奇怪怪的操作,安安心心的更新数组了。test('setting a property with an unobserved value should wrap with reactive', () => { const observed: any = reactive({}) const raw = {} observed.foo = raw expect(observed.foo).not.toBe(raw) expect(isReactive(observed.foo)).toBe(true)})
又要敲黑板了,这是通过 Proxy
实现响应式数据的巨大好处之二。在vue@2中,响应式数据必须一开始就声明好key,如果一开始不存在此属性值,也必须先设置一个默认值。通过现在这套技术方案,vue@3的响应式数据的属性值终于可以随时添加删除了。test('observing already observed value should return same Proxy', () => { const original = { foo: 1 } const observed = reactive(original) const observed2 = reactive(observed) expect(observed2).toBe(observed)})test('observing the same value multiple times should return same Proxy', () => { const original = { foo: 1 } const observed = reactive(original) const observed2 = reactive(original) expect(observed2).toBe(observed)})
这两个单测说明了,对于同一个原始数据,执行多次 reactive
或者嵌套执行 reactive
,返回的结果都是同一个相应数据。说明 reactive
文件内维持了一个缓存,以原始数据为key,以其响应数据为value,若该key已存在value,则直接返回value。那js基础OK的同学应该知道,通过 WeakMap
即可实现这样的结果。test('unwrap', () => { const original = { foo: 1 } const observed = reactive(original) expect(toRaw(observed)).toBe(original) expect(toRaw(original)).toBe(original)})
通过这个单测,了解了 toRaw
这个api,可以通过响应数据获取原始数据。那说明 reactive
文件内还需要维持另外一个 WeakMap
做反向映射。Symbol
(经本人测试,函数也不支持)。而对于内置一些的特殊类型,如 Promise
、RegExp
、Date
,这三个类型的数据传递给 reactive
时不会报错,会直接返回原始数据。test('markNonReactive', () => { const obj = reactive({ foo: { a: 1 }, bar: markNonReactive({ b: 2 }) }) expect(isReactive(obj.foo)).toBe(true) expect(isReactive(obj.bar)).toBe(false)})
这里引用了一个api- markNonReactive
,通过此api包裹的对象数据,不会成为响应式数据。这个api真实业务中应该使用比较少,做某些特殊的性能优化时可能会使用到。reactive
有了一定认识:它能接受一个对象或数组,返回新的响应数据。响应数据跟原始数据就跟影子一样,对任何一方的任何操作都能同步到对方身上。reactive
的返回值是个 UnwrapNestedRefs
类型,乍一看是一种特殊的 Ref
类型,那咱们就继续看看 ref
。(实际上这个UnwrapNestedRefs是为了获取嵌套Ref的泛型的类型,记住这个Unwrap是一个动词,这有点儿绕,以后讲源码解析时再阐述)it('should hold a value', () => { const a = ref(1) expect(a.value).toBe(1) a.value = 2 expect(a.value).toBe(2)})
那我们先看下 ref
函数的声明,传递任何数据,能返回一个 Ref
数据。Ref
数据的value值的类型不正是 reactive
函数的返回类型吗。只是 reactive
必须要求泛型继承于对象(在js中就是 reactive
传参需要是object),而 Ref
数据没有限制。也就是说, Ref
类型是基于 Reactive
数据的一种特殊数据类型,除了支持object外,还支持其他数据类型。ref
函数一个数字,也能返回一个 Ref
对象,其value值为当时传递的数字值,且允许修改这个value。it('should be reactive', () => { const a = ref(1) let dummy effect(() => { dummy = a.value }) expect(dummy).toBe(1) a.value = 2 expect(dummy).toBe(2)})
这个单测更有信息量了,突然多了个 effect
概念。先不管它是啥,反正给effect传递了一个函数,其内部做了一个赋值操作,将 ref
函数返回结果的value(a.value)赋值给dummy。然后这个函数会默认先执行一次,使得dummy变为1。而当a.value变化时,这个effect函数会重新执行,使得dummy变成最新的value值。reactive
时的疑惑:当响应数据变化时,如何通知其使用方?很明显,就是通过effect。每当 reactive
数据变化时,触发依赖其的effect方法执行。effect
函数传递一个响应函数;ref
函数也支持非对象数据,而Proxy仅支持对象。所以在本库 reactivity
中针对非对象数据会进行一层对象化的包装,再通过.value去取值。it('should make nested properties reactive', () => { const a = ref({ count: 1 }) let dummy effect(() => { dummy = a.value.count }) expect(dummy).toBe(1) a.value.count = 2 expect(dummy).toBe(2)})
传递给ref函数的原始数据变成了对象,对其代理数据的操作,也会触发effect执行。看完以后我就先产生了几个好奇:it('should make nested properties reactive', () => { const origin = { count: 1, b: { count: 1 } } const a = ref(origin) // 声明两个变量,dummy跟踪a.value.count,dummyB跟踪a.value.b.count let dummy, dummyB effect(() => { dummy = a.value.count }) effect(() => { dummyB = a.value.b.count }) expect(dummy).toBe(1) // 修改代理数据的第一层数据 a.value.count = 2 expect(dummy).toBe(2) // 修改代理对象的嵌套数据 expect(dummyB).toBe(1) a.value.b.count = 2 expect(dummyB).toBe(2) // 修改原始数据的第一层数据 origin.count = 10 expect(a.value.count).toBe(10) expect(dummy).toBe(2) // 修改原始数据的嵌套数据 origin.b.count = 10 expect(a.value.b.count).toBe(10) expect(dummyB).toBe(2) })
结果如我所料(其实最初是我试出来的,只是为了写文章顺畅写的如我所料):Ref
数据的更新,会触发依赖其的effect的执行。那 Reactive
数据呢?我们继续往下看。it('should work like a normal property when nested in a reactive object', () => { const a = ref(1) const obj = reactive({ a, b: { c: a, d: [a] } }) let dummy1 let dummy2 let dummy3 effect(() => { dummy1 = obj.a dummy2 = obj.b.c dummy3 = obj.b.d[0] }) expect(dummy1).toBe(1) expect(dummy2).toBe(1) expect(dummy3).toBe(1) a.value++ expect(dummy1).toBe(2) expect(dummy2).toBe(2) expect(dummy3).toBe(2) obj.a++ expect(dummy1).toBe(3) expect(dummy2).toBe(3) expect(dummy3).toBe(3)})
第四个单测,终于引入了 reactive
。在之前 reactive
的单测中,传递的都是简单的对象。在此处,传递的对象中的一些属性值是 Ref
数据。并且这样使用以后,这些 Ref
数据再也不需要用.value取值了,甚至是内部嵌套的 Ref
数据也不需要。利用TS的类型推导,我们可以清晰的看到。reactive
的返回类型为什么叫做 UnwrapNestedRefs<T>
了。由于泛型 T
可能是个 Ref<T>
,所以这个返回类型其实意思为:解开包裹着的嵌套 Ref
的泛型 T
。具体来说就是,如果传给 reactive
函数一个 Ref
数据,那函数执行后返回的数据类型是 Ref
数据的原始数据的数据类型。这个没怎么接触TS的人应该是不理解的,以后源码解析时再具体阐述吧。Reactive
数据,也会触发effect的更新。it('should unwrap nested values in types', () => { const a = { b: ref(0) } const c = ref(a) expect(typeof (c.value.b + 1)).toBe('number')})
第五个单测很有意思,我们发现对嵌套的 Ref
数据的取值,只需要最开始使用.value,内部的代理数据不需要重复调用.value。说明在上个单测中,向 reactive
函数传递的嵌套 Ref
数据能被解套,跟 reactive
函数其实是没关系的,是Ref
数据自身拥有的能力。其实根据TS type跟类型推导,我们也能看出来:const a = { b: ref(0), d: { b: ref(0), d: ref({ b: 0, d: { b: ref(0) } }) }}const c = ref(a)
反正就是套来套去,一下套一下又不套,根据TS类型推导,我们发现这种情况也毫无问题,只要最开始.value一次即可。test('isRef', () => { expect(isRef(ref(1))).toBe(true) expect(isRef(computed(() => 1))).toBe(true) expect(isRef(0)).toBe(false) // an object that looks like a ref isn't necessarily a ref expect(isRef({ value: 0 })).toBe(false)})
这个单测没太多好讲,不过也有些有用的信息, computed
虽然还没接触,但我们知道了,它的返回结果也是个ref数据。换言之,如果有effect是依赖 computed
的返回数据的,那当它改变时,effect也会执行。test('toRefs', () => { const a = reactive({ x: 1, y: 2 }) const { x, y } = toRefs(a) expect(isRef(x)).toBe(true) expect(isRef(y)).toBe(true) expect(x.value).toBe(1) expect(y.value).toBe(2) // source -> proxy a.x = 2 a.y = 3 expect(x.value).toBe(2) expect(y.value).toBe(3) // proxy -> source x.value = 3 y.value = 4 expect(a.x).toBe(3) expect(a.y).toBe(4) // reactivity let dummyX, dummyY effect(() => { dummyX = x.value dummyY = y.value }) expect(dummyX).toBe(x.value) expect(dummyY).toBe(y.value) // mutating source should trigger effect using the proxy refs a.x = 4 a.y = 5 expect(dummyX).toBe(4) expect(dummyY).toBe(5)})
这个单测是针对 toRefs
这个api的。根据单测来看, toRefs
跟 ref
的区别就是, ref
会将传入的数据变成 Ref
类型,而 toRefs
要求传入的数据必须是object,然后将此对象的第一层数据转为 Ref
类型。也不知道它能干什么用,知道效果是怎么样就行。reactive
数据触发effect方法。effect
的行为其实从上述的测试文件中,我们已经能明白了。主要就是可以监听响应式数据的变化,触发监听函数的执行。事情描述虽然简单,但 effect
的单测量却很多,有39个用例,600多行代码,很多边界情况的考虑。所以针对effect,我就不一个个列举了。我先帮大家看一遍,然后总结分成几个小点,直接总结关键结论,有必要的话,再贴上相应测试代码。reactive
可以观察原型链上数据的变化,且被effect函数监听到,也可以继承原型链上的属性访问器(get/set)。it('should observe properties on the prototype chain', () => { let dummy const counter = reactive({ num: 0 }) const parentCounter = reactive({ num: 2 }) Object.setPrototypeOf(counter, parentCounter) effect(() => (dummy = counter.num)) expect(dummy).toBe(0) delete counter.num expect(dummy).toBe(2) parentCounter.num = 4 expect(dummy).toBe(4) counter.num = 3 expect(dummy).toBe(3)})
Symbol.isConcatSpreadable
(日常使用基本不会涉及)it('should not observe set operations without a value change', () => { let hasDummy, getDummy const obj = reactive({ prop: 'value' }) const getSpy = jest.fn(() => (getDummy = obj.prop)) const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj)) effect(getSpy) effect(hasSpy) expect(getDummy).toBe('value') expect(hasDummy).toBe(true) obj.prop = 'value' expect(getSpy).toHaveBeenCalledTimes(1) expect(hasSpy).toHaveBeenCalledTimes(1) expect(getDummy).toBe('value') expect(hasDummy).toBe(true)})
it('should return a new reactive version of the function', () => { function greet() { return 'Hello World' } const effect1 = effect(greet) const effect2 = effect(greet) expect(typeof effect1).toBe('function') expect(typeof effect2).toBe('function') expect(effect1).not.toBe(greet) expect(effect1).not.toBe(effect2)})
stop
api,终止监听函数继续监听。(感觉可以再加个 start
,有兴趣的同学可以给小右提PR)it('stop', () => { let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) obj.prop = 2 expect(dummy).toBe(2) stop(runner) obj.prop = 3 expect(dummy).toBe(2) // stopped effect should still be manually callable runner() expect(dummy).toBe(3)})
it('should avoid implicit infinite recursive loops with itself', () => { const counter = reactive({ num: 0 }) const counterSpy = jest.fn(() => counter.num++) effect(counterSpy) expect(counter.num).toBe(1) expect(counterSpy).toHaveBeenCalledTimes(1) counter.num = 4 expect(counter.num).toBe(5) expect(counterSpy).toHaveBeenCalledTimes(2)})it('should allow explicitly recursive raw function loops', () => { const counter = reactive({ num: 0 }) const numSpy = jest.fn(() => { counter.num++ if (counter.num < 10) { numSpy() } }) effect(numSpy) expect(counter.num).toEqual(10) expect(numSpy).toHaveBeenCalledTimes(10)})
obj.run
为 false
时, conditionalSpy
重新执行一次后更新了监听依赖,后续无论 obj.prop
如何变化,监听函数也不会再执行。it('should not be triggered by mutating a property, which is used in an inactive branch', () => { let dummy const obj = reactive({ prop: 'value', run: true }) const conditionalSpy = jest.fn(() => { dummy = obj.run ? obj.prop : 'other' }) effect(conditionalSpy) expect(dummy).toBe('value') expect(conditionalSpy).toHaveBeenCalledTimes(1) obj.run = false expect(dummy).toBe('other') expect(conditionalSpy).toHaveBeenCalledTimes(2) obj.prop = 'value2' expect(dummy).toBe('other') expect(conditionalSpy).toHaveBeenCalledTimes(2)})
effect
还能接受第二参数 ReactiveEffectOptions
,参数如下:export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean scheduler?: (run: Function) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void}
computed
有关系,先放着。stop
终止监听函数时触发的事件。effect
的逻辑虽然很多,但核心概念还是好理解的,需要关注的是内部一些特殊的优化,将来阅读源码时需要重点看看。接下来还有个 computed
我们接触了但还没阅读。reactivity
中它具体如何。it('should return updated value', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) expect(cValue.value).toBe(undefined) value.foo = 1 expect(cValue.value).toBe(1)})
向 computed
传递一个getter函数,函数内部依赖了一个 Reactive
数据,函数执行后返回一个计算对象,其value为函数的返回值。当其依赖的 Reactive
数据变更时,计算数据能保持同步,好像 Ref
呀。其实在 ref
测试文件中我们已经知道了,computed的返回结果也是一种 Ref
数据。ComputedRef
继承于 Ref
,相比 Ref
多了一个只读的 effect 属性,类型是 ReactiveEffect
。那能猜到,此处的effect属性的值应该就是我们传给 computed
的计算函数,再被 effect
函数执行后返回的结果。另外其 value
是只读的,说明 computed
的返回结果的value值是只读的。it('should compute lazily', () => { const value = reactive<{ foo?: number }>({}) const getter = jest.fn(() => value.foo) const cValue = computed(getter) // lazy expect(getter).not.toHaveBeenCalled() expect(cValue.value).toBe(undefined) expect(getter).toHaveBeenCalledTimes(1) // should not compute again cValue.value expect(getter).toHaveBeenCalledTimes(1) // should not compute until needed value.foo = 1 expect(getter).toHaveBeenCalledTimes(1) // now it should compute expect(cValue.value).toBe(1) expect(getter).toHaveBeenCalledTimes(2) // should not compute again cValue.value expect(getter).toHaveBeenCalledTimes(2)})
这个单测告诉了我们 computed
很多特性:effect
,向 computed
传递的 getter
函数,并不会立即执行,当真正使用该数据时才会执行。getter
函数,且 getter
函数依赖的数据变更时也不会重新触发,当且仅当依赖数据变更后,再次使用计算数据时,才会真正触发 getter
函数。ComputedRef
的effect属性,是通过向 effect
方法传递 getter
函数生成的监听函数。但是在 effect
单测中,一旦依赖数据变更,这个监听函数就会立即执行,这就跟此处 computed
的表现不一致了。这其中一定有猫腻!Effect
的最后,我们发现 effect
函数第二个参数是个配置项,而其中有个配置就叫computed,在单测中也没覆盖到。估计就是这个配置项,实现了此处计算数据的延迟计算。it('should trigger effect', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) let dummy effect(() => { dummy = cValue.value }) expect(dummy).toBe(undefined) value.foo = 1 expect(dummy).toBe(1)})
这个单测证明了我们在 Ref
一章中提出的猜想:如果有effect是依赖 computed
的返回数据的,那当它改变时,effect也会执行。computed
返回数据虽然没变更,但是其依赖数据变更了呢?这样会不会导致 effect
执行呢?我猜想如果 computed
的值不变的话,是不会导致监听函数重新执行的,于是改变下单测:it('should trigger effect', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo ? true : false) let dummy const reactiveEffect = jest.fn(() => { dummy = cValue.value }) effect(reactiveEffect) expect(dummy).toBe(false) expect(reactiveEffect).toHaveBeenCalledTimes(1) value.foo = 1 expect(dummy).toBe(true) expect(reactiveEffect).toHaveBeenCalledTimes(2) value.foo = 2 expect(dummy).toBe(true) expect(reactiveEffect).toHaveBeenCalledTimes(2)})
然后发现我错了。 reactiveEffect
依赖于 cValue
,cValue
依赖于 value
,只要 value
变更,不管 cValue
有没有改变,都会重新触发 reactiveEffect
。感觉这里可以优化下,有兴趣的同学可以去提PR。it('should work when chained', () => { const value = reactive({ foo: 0 }) const c1 = computed(() => value.foo) const c2 = computed(() => c1.value + 1) expect(c2.value).toBe(1) expect(c1.value).toBe(0) value.foo++ expect(c2.value).toBe(2) expect(c1.value).toBe(1)})
这个单测说明了 computed
的 getter
函数可以依赖于另外的 computed
数据。computed
。传达的概念就是:使用 computed
数据跟使用正常的响应数据差不多,都能正确的触发监听函数的执行。it('should no longer update when stopped', () => { const value = reactive<{ foo?: number }>({}) const cValue = computed(() => value.foo) let dummy effect(() => { dummy = cValue.value }) expect(dummy).toBe(undefined) value.foo = 1 expect(dummy).toBe(1) stop(cValue.effect) value.foo = 2 expect(dummy).toBe(1)})
这个单测又引入了 stop
这个api,通过 stop(cValue.effect)
终止了此计算数据的响应更新。it('should support setter', () => { const n = ref(1) const plusOne = computed({ get: () => n.value + 1, set: val => { n.value = val - 1 } }) expect(plusOne.value).toBe(2) n.value++ expect(plusOne.value).toBe(3) plusOne.value = 0 expect(n.value).toBe(-1)})it('should trigger effect w/ setter', () => { const n = ref(1) const plusOne = computed({ get: () => n.value + 1, set: val => { n.value = val - 1 } }) let dummy effect(() => { dummy = n.value }) expect(dummy).toBe(1) plusOne.value = 0 expect(dummy).toBe(-1)})
这两个单测比较重要。之前我们 computed
只是传递 getter
函数,且其 value
是只读的,无法直接修改返回值。这里让我们知道, computed
也可以传递一个包含get/set两个方法的对象。get就是 getter
函数,比较好理解。 setter
函数接收的入参即是赋给 comptued
value数据的值。所以在上面用例中,plusOne.value = 0
,使得 n.value = 0 - 1
,再触发 dummy
变为-1。reactivity
系统的概念,还剩下 readonly
跟 collections
。 readonly
单测文件特别多,但实际上概念很简单的,就是 reactive
的只读版本。 collections
单测是为了覆盖 Map
、Set
、WeakMap
、WeakSet
的响应更新的,暂时不看的问题应该也不大。Ref
类型。reactiveEffect
。若监听函数内部依赖了reactive数据,当这些数据变更时会触发监听函数。关键词:响应,系统