注意:在我写文章的时候,可能代码已有变动。在您读文章的时候,代码更有可能变动,若有不一致且有会对源码实现或理解产生重大不一致,欢迎指出,万分感谢。
javascript
10.5号,国庆佳节,小右男神发布了vue@3.0.0的alpha版代码。反正也没啥事干,最近也在学TypeScript,正好看看男神的代码,学习一下。vue
从入口文件packages/vue/index进去,初极狭,7行代码。复寻数个文件,直至runtime-core,豁然开朗。注释行行,API俨然。算了,编不下去了,总之就是代码开始变多了。感受国庆想看完是确定不可能的,那就挑个总是面试时问别人的双向绑定原理的核心实现吧。java
你们应该都知道,Vue3要利用Proxy替换defineProperty来实现数据的响应更新,那具体是怎么实现呢?打开源码文件目录,一眼就能知道,核心在于packages/reactivity。react
点开它的Readme,经过Google翻译,咱们能明白它的大体意思是:git
这个包会内嵌到vue的渲染器中(@vue/runtime-dom)。不过它也能够单独发布且被第三方引用(不依赖vue)。可是呢,大家也别瞎用,若是大家的渲染器是暴露给框架使用者的,它可能已经内置了一套响应机制,这跟我们的reactivity是彻底的两套,不必定兼容的(说的就是你,react-dom)。github
关于它的api呢,你们就先看看源码或者看看types吧。注意:除了
Map
,WeakMap
,Set
andWeakSet
外,内置的一些对象是不能被观测的(例如:Date
,RegExp
等)。面试
唔,单根据Readme,没法清晰的知道,它具体是怎么样的。毕竟也是alpha版。那咱们仍是听它的,直接撸源码吧。typescript
从reactivity的入口文件进去,发现它只是暴露了6个文件内的apis。分别是: ref
、reactive
、computed
、effect
、lock
、operations
。其中 lock
跟 operations
很简单, lock
文件内部就是两个控制锁开关变量的方法, operations
内部就是对数据操做的类型的枚举。api
因此reactivity的重点就在ref
、reactive
、computed
、effect
这四个文件,但这四个文件就没这么简单了。我花了半天,从头至尾的撸了一遍,发现每一个字母我都认识;每一个单词,借助google,我也都知道;基本全部的表达式,我这半吊子的TypeScript水平也都能理解。可是,当它们组成一个个函数的时候,我就有点儿懵逼了.....ref
里引了 reactive
, reactive
里又引用了 ref
,再加上函数内部一下奇奇怪怪的操做,绕两下便迷糊了。数组
我总结了下,很大缘由是我不知道这几个关键的api,究竟是要作啥。源码我不懂、api的含义我也不懂。咱们知道,单个二元一次方程,是求不出解的。
那怎么办呢?其实还有一个方程,那就是单测。从单测开始读,是一个极好的阅读源码的办法。不只能快速知道api的含义跟用法,还能知道不少边界状况。在阅读的过程当中,还会想,若是是本身的话,会怎么去实现,后续能加深对源码的认识跟学习。
由于我小撸了下源码,因此大体能知道的阅读顺序。固然,根据代码行数,咱们也能估摸个大体顺序。这里我就直接给结论,建议阅读顺序:reactive -> ref -> effect -> computed -> readonly -> collections
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的响应式数据的属性值终于能够随时添加删除了。
第8、九个单测
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
作反向映射。
第十一个单测,不贴代码了,本单测列举了不可成为响应数据的数据类型,即JS五种基本数据类型+ 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
有了必定认识:它能接受一个对象或数组,返回新的响应数据。响应数据跟原始数据就跟影子同样,对任何一方的任何操做都能同步到对方身上。
但这...好像没什么厉害之处。但从单测的表现来讲,就是基于Proxy,作了一些边界跟嵌套上的处理。那这就引出了一个很是关键的问题:**在vue@3中,它是如何通知视图更新的?或者说,当响应数据变动时,它是如何通知它的使用方,要作一些操做的?**这些行为确定是封装在Proxy的set/get等各种handler中。但目前还不知道,只能先继续往下看其余单测啦。
因为最开始,咱们就知道了, reactive
的返回值是个 UnwrapNestedRefs
类型,乍一看是一种特殊的 Ref
类型,那我们就继续看看 ref
。(实际上这个UnwrapNestedRefs是为了获取嵌套Ref的泛型的类型,记住这个Unwrap是一个动词,这有点儿绕,之后讲源码解析时再阐述)
那先看ref的第一个单测:
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值。
也就是说,若是向effect传递一个方法,会当即执行一次,每当其内部依赖的ref数据发生变动时,会从新执行。这就解开了以前阅读 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执行。看完之后我就先产生了几个好奇:
因而我假使1.能够嵌套,2. 会同步,3.不会触发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一次便可。
不过这个能力在小右10月5号的发布的第一个版本是有欠缺的,它不能推导嵌套超过9层的数据。这个commit解决了这个问题,对TS类型推导有兴趣的同窗能够看下。
第六个单测
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
类型。也不知道它能干什么用,知道效果是怎么样就行。
至此ref的单测看完了,大体能够感觉到ref最重要的目的就是,实现非对象数据的劫持。其余的话,彷佛没有其余特殊的用处。实际上在effect的测试文件中,目前也只测试了 reactive
数据触发effect方法。
那下面咱们看看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
咱们接触了但还没阅读。
计算属性。这个写过vue的同窗,应该的都能知道是什么意思。咱们看看在 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
数据。
查看TS Type,果真 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
的响应更新的,暂时不看的问题应该也不大。
梳理完之后,咱们应该对内部的主要api有了清晰的认识,咱们再总结复习一下:
reactive: 本库的核心方法,传递一个object类型的原始数据,经过Proxy,返回一个代理数据。在这过程当中,劫持了原始数据的任何读写操做。进而实现改变代理数据时,能触发依赖其的监听函数effect。
ref:这是最影响代码阅读的一个文件(粗看代码很容易搞晕它跟reactive的关系),但要想真正明白它,又须要仔细阅读代码。建议在理清其余逻辑前,千万别管它....当它不存在。只要知道,这个文件最重要的做用就是提供了一套 Ref
类型。
effect:接受一个函数,返回一个新的监听函数 reactiveEffect
。若监听函数内部依赖了reactive数据,当这些数据变动时会触发监听函数。
computed: 计算数据,接受一个getter函数或者包含get/set行为的对象,返回一个响应式的数据。它如有变动,也会触发reactiveEffect。
最后画了张大体的图,方便记忆回顾。
不过这张图,我还不保证对,由于源码我还没好好撸完。这周我再抽时间,写篇真正的源码解析。
本文做者:蚂蚁保险-体验技术组-阿相
掘金地址:相学长