10.5號,國慶佳節(jié),小右男神發(fā)布了vue@3.0." />
時(shí)間:2023-05-09 15:45:02 | 來源:網(wǎng)站運(yùn)營
時(shí)間:2023-05-09 15:45:02 來源:網(wǎng)站運(yùn)營
Vue3響應(yīng)式系統(tǒng)源碼解析-單測篇:注意:在我寫文章的時(shí)候,可能代碼已有變更。在您讀文章的時(shí)候,代碼更有可能變更,如有不一致且有會對源碼實(shí)現(xiàn)或理解產(chǎn)生重大不一致,歡迎指出,萬分感謝。
這個(gè)包會內(nèi)嵌到vue的渲染器中(@vue/runtime-dom)。不過它也可以單獨(dú)發(fā)布且被第三方引用(不依賴vue)。但是呢,你們也別瞎用,如果你們的渲染器是暴露給框架使用者的,它可能已經(jīng)內(nèi)置了一套響應(yīng)機(jī)制,這跟咱們的reactivity是完全的兩套,不一定兼容的(說的就是你,react-dom)。唔,單根據(jù)Readme,無法清晰的知道,它具體是怎么樣的。畢竟也是alpha版。那我們還是聽它的,直接擼源碼吧。
關(guān)于它的api呢,大家就先看看源碼或者看看types吧。注意:除了Map
,WeakMap
,Set
andWeakSet
外,內(nèi)置的一些對象是不能被觀測的(例如:Date
,RegExp
等)。
ref
、reactive
、computed
、effect
、lock
、operations
。其中 lock
跟 operations
很簡單, lock
文件內(nèi)部就是兩個(gè)控制鎖開關(guān)變量的方法, operations
內(nèi)部就是對數(shù)據(jù)操作的類型的枚舉。ref
、reactive
、computed
、effect
這四個(gè)文件,但這四個(gè)文件就沒這么簡單了。我花了半天,從頭到尾的擼了一遍,發(fā)現(xiàn)每個(gè)字母我都認(rèn)識;每個(gè)單詞,借助google,我也都知道;基本所有的表達(dá)式,我這半吊子的TypeScript水平也都能理解。但是,當(dāng)它們組成一個(gè)個(gè)函數(shù)的時(shí)候,我就有點(diǎn)兒懵逼了.....ref
里引了 reactive
, reactive
里又引用了 ref
,再加上函數(shù)內(nèi)部一下奇奇怪怪的操作,繞兩下便迷糊了。reactive
顧名思義,響應(yīng)式,意味著 reactive
數(shù)據(jù)是響應(yīng)式數(shù)據(jù),從名字上就說明了它是本庫的核心。那我們先來看看它有什么樣的能力。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
傳遞一個(gè)對象,會返回一個(gè)新對象,兩個(gè)對象類型一致,數(shù)據(jù)長得一致,但引用不同。那我們頓時(shí)就明白了,這肯定是利用了Proxy!vue@3響應(yīng)式系統(tǒng)核心中的核心。reactive
的聲明:reactive
只接受對象數(shù)據(jù),返回的是一個(gè) UnwrapNestedRefs
數(shù)據(jù)類型,但它到底是個(gè)啥,也不知道,以后再說。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
接收了一個(gè)數(shù)組(數(shù)組自然也是object),返回的新數(shù)組,不全等于原數(shù)組,但數(shù)據(jù)一致。跟單測一中的對象情況表現(xiàn)一致。不過這個(gè)單測沒考慮嵌套的,我補(bǔ)充一下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'])})
說明返回的新數(shù)據(jù),只要屬性值還是個(gè)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
執(zhí)行后返回的響應(yīng)數(shù)據(jù),對其做任何寫/刪操作,都能同步地同步到原始數(shù)據(jù)。那如果反過來,直接更改原始數(shù)據(jù)呢?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)})
我們發(fā)現(xiàn)直接修改原始數(shù)據(jù),響應(yīng)數(shù)據(jù)也能獲取的最新數(shù)據(jù)。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)})
第六個(gè)單測證明了通過 Proxy
實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)的巨大好處之一:可以劫持?jǐn)?shù)組的所有數(shù)據(jù)變更。還記得在vue@2中,需要手動set數(shù)組嗎?在vue@3中,終于不用做一些奇奇怪怪的操作,安安心心的更新數(shù)組了。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
實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)的巨大好處之二。在vue@2中,響應(yīng)式數(shù)據(jù)必須一開始就聲明好key,如果一開始不存在此屬性值,也必須先設(shè)置一個(gè)默認(rèn)值。通過現(xiàn)在這套技術(shù)方案,vue@3的響應(yīng)式數(shù)據(jù)的屬性值終于可以隨時(shí)添加刪除了。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)})
這兩個(gè)單測說明了,對于同一個(gè)原始數(shù)據(jù),執(zhí)行多次 reactive
或者嵌套執(zhí)行 reactive
,返回的結(jié)果都是同一個(gè)相應(yīng)數(shù)據(jù)。說明 reactive
文件內(nèi)維持了一個(gè)緩存,以原始數(shù)據(jù)為key,以其響應(yīng)數(shù)據(jù)為value,若該key已存在value,則直接返回value。那js基礎(chǔ)OK的同學(xué)應(yīng)該知道,通過 WeakMap
即可實(shí)現(xiàn)這樣的結(jié)果。test('unwrap', () => { const original = { foo: 1 } const observed = reactive(original) expect(toRaw(observed)).toBe(original) expect(toRaw(original)).toBe(original)})
通過這個(gè)單測,了解了 toRaw
這個(gè)api,可以通過響應(yīng)數(shù)據(jù)獲取原始數(shù)據(jù)。那說明 reactive
文件內(nèi)還需要維持另外一個(gè) WeakMap
做反向映射。Symbol
(經(jīng)本人測試,函數(shù)也不支持)。而對于內(nèi)置一些的特殊類型,如 Promise
、RegExp
、Date
,這三個(gè)類型的數(shù)據(jù)傳遞給 reactive
時(shí)不會報(bào)錯(cuò),會直接返回原始數(shù)據(jù)。test('markNonReactive', () => { const obj = reactive({ foo: { a: 1 }, bar: markNonReactive({ b: 2 }) }) expect(isReactive(obj.foo)).toBe(true) expect(isReactive(obj.bar)).toBe(false)})
這里引用了一個(gè)api- markNonReactive
,通過此api包裹的對象數(shù)據(jù),不會成為響應(yīng)式數(shù)據(jù)。這個(gè)api真實(shí)業(yè)務(wù)中應(yīng)該使用比較少,做某些特殊的性能優(yōu)化時(shí)可能會使用到。reactive
有了一定認(rèn)識:它能接受一個(gè)對象或數(shù)組,返回新的響應(yīng)數(shù)據(jù)。響應(yīng)數(shù)據(jù)跟原始數(shù)據(jù)就跟影子一樣,對任何一方的任何操作都能同步到對方身上。reactive
的返回值是個(gè) UnwrapNestedRefs
類型,乍一看是一種特殊的 Ref
類型,那咱們就繼續(xù)看看 ref
。(實(shí)際上這個(gè)UnwrapNestedRefs是為了獲取嵌套Ref的泛型的類型,記住這個(gè)Unwrap是一個(gè)動詞,這有點(diǎn)兒繞,以后講源碼解析時(shí)再闡述)it('should hold a value', () => { const a = ref(1) expect(a.value).toBe(1) a.value = 2 expect(a.value).toBe(2)})
那我們先看下 ref
函數(shù)的聲明,傳遞任何數(shù)據(jù),能返回一個(gè) Ref
數(shù)據(jù)。Ref
數(shù)據(jù)的value值的類型不正是 reactive
函數(shù)的返回類型嗎。只是 reactive
必須要求泛型繼承于對象(在js中就是 reactive
傳參需要是object),而 Ref
數(shù)據(jù)沒有限制。也就是說, Ref
類型是基于 Reactive
數(shù)據(jù)的一種特殊數(shù)據(jù)類型,除了支持object外,還支持其他數(shù)據(jù)類型。ref
函數(shù)一個(gè)數(shù)字,也能返回一個(gè) Ref
對象,其value值為當(dāng)時(shí)傳遞的數(shù)字值,且允許修改這個(gè)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)})
這個(gè)單測更有信息量了,突然多了個(gè) effect
概念。先不管它是啥,反正給effect傳遞了一個(gè)函數(shù),其內(nèi)部做了一個(gè)賦值操作,將 ref
函數(shù)返回結(jié)果的value(a.value)賦值給dummy。然后這個(gè)函數(shù)會默認(rèn)先執(zhí)行一次,使得dummy變?yōu)?。而當(dāng)a.value變化時(shí),這個(gè)effect函數(shù)會重新執(zhí)行,使得dummy變成最新的value值。reactive
時(shí)的疑惑:當(dāng)響應(yīng)數(shù)據(jù)變化時(shí),如何通知其使用方?很明顯,就是通過effect。每當(dāng) reactive
數(shù)據(jù)變化時(shí),觸發(fā)依賴其的effect方法執(zhí)行。effect
函數(shù)傳遞一個(gè)響應(yīng)函數(shù);ref
函數(shù)也支持非對象數(shù)據(jù),而Proxy僅支持對象。所以在本庫 reactivity
中針對非對象數(shù)據(jù)會進(jìn)行一層對象化的包裝,再通過.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函數(shù)的原始數(shù)據(jù)變成了對象,對其代理數(shù)據(jù)的操作,也會觸發(fā)effect執(zhí)行??赐暌院笪揖拖犬a(chǎn)生了幾個(gè)好奇:it('should make nested properties reactive', () => { const origin = { count: 1, b: { count: 1 } } const a = ref(origin) // 聲明兩個(gè)變量,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) // 修改代理數(shù)據(jù)的第一層數(shù)據(jù) a.value.count = 2 expect(dummy).toBe(2) // 修改代理對象的嵌套數(shù)據(jù) expect(dummyB).toBe(1) a.value.b.count = 2 expect(dummyB).toBe(2) // 修改原始數(shù)據(jù)的第一層數(shù)據(jù) origin.count = 10 expect(a.value.count).toBe(10) expect(dummy).toBe(2) // 修改原始數(shù)據(jù)的嵌套數(shù)據(jù) origin.b.count = 10 expect(a.value.b.count).toBe(10) expect(dummyB).toBe(2) })
結(jié)果如我所料(其實(shí)最初是我試出來的,只是為了寫文章順暢寫的如我所料):Ref
數(shù)據(jù)的更新,會觸發(fā)依賴其的effect的執(zhí)行。那 Reactive
數(shù)據(jù)呢?我們繼續(xù)往下看。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)})
第四個(gè)單測,終于引入了 reactive
。在之前 reactive
的單測中,傳遞的都是簡單的對象。在此處,傳遞的對象中的一些屬性值是 Ref
數(shù)據(jù)。并且這樣使用以后,這些 Ref
數(shù)據(jù)再也不需要用.value取值了,甚至是內(nèi)部嵌套的 Ref
數(shù)據(jù)也不需要。利用TS的類型推導(dǎo),我們可以清晰的看到。reactive
的返回類型為什么叫做 UnwrapNestedRefs<T>
了。由于泛型 T
可能是個(gè) Ref<T>
,所以這個(gè)返回類型其實(shí)意思為:解開包裹著的嵌套 Ref
的泛型 T
。具體來說就是,如果傳給 reactive
函數(shù)一個(gè) Ref
數(shù)據(jù),那函數(shù)執(zhí)行后返回的數(shù)據(jù)類型是 Ref
數(shù)據(jù)的原始數(shù)據(jù)的數(shù)據(jù)類型。這個(gè)沒怎么接觸TS的人應(yīng)該是不理解的,以后源碼解析時(shí)再具體闡述吧。Reactive
數(shù)據(jù),也會觸發(fā)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')})
第五個(gè)單測很有意思,我們發(fā)現(xiàn)對嵌套的 Ref
數(shù)據(jù)的取值,只需要最開始使用.value,內(nèi)部的代理數(shù)據(jù)不需要重復(fù)調(diào)用.value。說明在上個(gè)單測中,向 reactive
函數(shù)傳遞的嵌套 Ref
數(shù)據(jù)能被解套,跟 reactive
函數(shù)其實(shí)是沒關(guān)系的,是Ref
數(shù)據(jù)自身擁有的能力。其實(shí)根據(jù)TS type跟類型推導(dǎo),我們也能看出來:const a = { b: ref(0), d: { b: ref(0), d: ref({ b: 0, d: { b: ref(0) } }) }}const c = ref(a)
反正就是套來套去,一下套一下又不套,根據(jù)TS類型推導(dǎo),我們發(fā)現(xiàn)這種情況也毫無問題,只要最開始.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)})
這個(gè)單測沒太多好講,不過也有些有用的信息, computed
雖然還沒接觸,但我們知道了,它的返回結(jié)果也是個(gè)ref數(shù)據(jù)。換言之,如果有effect是依賴 computed
的返回?cái)?shù)據(jù)的,那當(dāng)它改變時(shí),effect也會執(zhí)行。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)})
這個(gè)單測是針對 toRefs
這個(gè)api的。根據(jù)單測來看, toRefs
跟 ref
的區(qū)別就是, ref
會將傳入的數(shù)據(jù)變成 Ref
類型,而 toRefs
要求傳入的數(shù)據(jù)必須是object,然后將此對象的第一層數(shù)據(jù)轉(zhuǎn)為 Ref
類型。也不知道它能干什么用,知道效果是怎么樣就行。reactive
數(shù)據(jù)觸發(fā)effect方法。effect
的行為其實(shí)從上述的測試文件中,我們已經(jīng)能明白了。主要就是可以監(jiān)聽響應(yīng)式數(shù)據(jù)的變化,觸發(fā)監(jiān)聽函數(shù)的執(zhí)行。事情描述雖然簡單,但 effect
的單測量卻很多,有39個(gè)用例,600多行代碼,很多邊界情況的考慮。所以針對effect,我就不一個(gè)個(gè)列舉了。我先幫大家看一遍,然后總結(jié)分成幾個(gè)小點(diǎn),直接總結(jié)關(guān)鍵結(jié)論,有必要的話,再貼上相應(yīng)測試代碼。reactive
可以觀察原型鏈上數(shù)據(jù)的變化,且被effect函數(shù)監(jiān)聽到,也可以繼承原型鏈上的屬性訪問器(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,終止監(jiān)聽函數(shù)繼續(xù)監(jiān)聽。(感覺可以再加個(gè) start
,有興趣的同學(xué)可以給小右提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
時(shí), conditionalSpy
重新執(zhí)行一次后更新了監(jiān)聽依賴,后續(xù)無論 obj.prop
如何變化,監(jiān)聽函數(shù)也不會再執(zhí)行。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
還能接受第二參數(shù) ReactiveEffectOptions
,參數(shù)如下:export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean scheduler?: (run: Function) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void}
computed
有關(guān)系,先放著。stop
終止監(jiān)聽函數(shù)時(shí)觸發(fā)的事件。effect
的邏輯雖然很多,但核心概念還是好理解的,需要關(guān)注的是內(nèi)部一些特殊的優(yōu)化,將來閱讀源碼時(shí)需要重點(diǎn)看看。接下來還有個(gè) 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
傳遞一個(gè)getter函數(shù),函數(shù)內(nèi)部依賴了一個(gè) Reactive
數(shù)據(jù),函數(shù)執(zhí)行后返回一個(gè)計(jì)算對象,其value為函數(shù)的返回值。當(dāng)其依賴的 Reactive
數(shù)據(jù)變更時(shí),計(jì)算數(shù)據(jù)能保持同步,好像 Ref
呀。其實(shí)在 ref
測試文件中我們已經(jīng)知道了,computed的返回結(jié)果也是一種 Ref
數(shù)據(jù)。ComputedRef
繼承于 Ref
,相比 Ref
多了一個(gè)只讀的 effect 屬性,類型是 ReactiveEffect
。那能猜到,此處的effect屬性的值應(yīng)該就是我們傳給 computed
的計(jì)算函數(shù),再被 effect
函數(shù)執(zhí)行后返回的結(jié)果。另外其 value
是只讀的,說明 computed
的返回結(jié)果的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)})
這個(gè)單測告訴了我們 computed
很多特性:effect
,向 computed
傳遞的 getter
函數(shù),并不會立即執(zhí)行,當(dāng)真正使用該數(shù)據(jù)時(shí)才會執(zhí)行。getter
函數(shù),且 getter
函數(shù)依賴的數(shù)據(jù)變更時(shí)也不會重新觸發(fā),當(dāng)且僅當(dāng)依賴數(shù)據(jù)變更后,再次使用計(jì)算數(shù)據(jù)時(shí),才會真正觸發(fā) getter
函數(shù)。ComputedRef
的effect屬性,是通過向 effect
方法傳遞 getter
函數(shù)生成的監(jiān)聽函數(shù)。但是在 effect
單測中,一旦依賴數(shù)據(jù)變更,這個(gè)監(jiān)聽函數(shù)就會立即執(zhí)行,這就跟此處 computed
的表現(xiàn)不一致了。這其中一定有貓膩!Effect
的最后,我們發(fā)現(xiàn) effect
函數(shù)第二個(gè)參數(shù)是個(gè)配置項(xiàng),而其中有個(gè)配置就叫computed,在單測中也沒覆蓋到。估計(jì)就是這個(gè)配置項(xiàng),實(shí)現(xiàn)了此處計(jì)算數(shù)據(jù)的延遲計(jì)算。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)})
這個(gè)單測證明了我們在 Ref
一章中提出的猜想:如果有effect是依賴 computed
的返回?cái)?shù)據(jù)的,那當(dāng)它改變時(shí),effect也會執(zhí)行。computed
返回?cái)?shù)據(jù)雖然沒變更,但是其依賴數(shù)據(jù)變更了呢?這樣會不會導(dǎo)致 effect
執(zhí)行呢?我猜想如果 computed
的值不變的話,是不會導(dǎo)致監(jiān)聽函數(shù)重新執(zhí)行的,于是改變下單測: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)})
然后發(fā)現(xiàn)我錯(cuò)了。 reactiveEffect
依賴于 cValue
,cValue
依賴于 value
,只要 value
變更,不管 cValue
有沒有改變,都會重新觸發(fā) reactiveEffect
。感覺這里可以優(yōu)化下,有興趣的同學(xué)可以去提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)})
這個(gè)單測說明了 computed
的 getter
函數(shù)可以依賴于另外的 computed
數(shù)據(jù)。computed
。傳達(dá)的概念就是:使用 computed
數(shù)據(jù)跟使用正常的響應(yīng)數(shù)據(jù)差不多,都能正確的觸發(fā)監(jiān)聽函數(shù)的執(zhí)行。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)})
這個(gè)單測又引入了 stop
這個(gè)api,通過 stop(cValue.effect)
終止了此計(jì)算數(shù)據(jù)的響應(yīng)更新。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)})
這兩個(gè)單測比較重要。之前我們 computed
只是傳遞 getter
函數(shù),且其 value
是只讀的,無法直接修改返回值。這里讓我們知道, computed
也可以傳遞一個(gè)包含get/set兩個(gè)方法的對象。get就是 getter
函數(shù),比較好理解。 setter
函數(shù)接收的入?yún)⒓词琴x給 comptued
value數(shù)據(jù)的值。所以在上面用例中,plusOne.value = 0
,使得 n.value = 0 - 1
,再觸發(fā) dummy
變?yōu)?1。reactivity
系統(tǒng)的概念,還剩下 readonly
跟 collections
。 readonly
單測文件特別多,但實(shí)際上概念很簡單的,就是 reactive
的只讀版本。 collections
單測是為了覆蓋 Map
、Set
、WeakMap
、WeakSet
的響應(yīng)更新的,暫時(shí)不看的問題應(yīng)該也不大。Ref
類型。reactiveEffect
。若監(jiān)聽函數(shù)內(nèi)部依賴了reactive數(shù)據(jù),當(dāng)這些數(shù)據(jù)變更時(shí)會觸發(fā)監(jiān)聽函數(shù)。關(guān)鍵詞:響應(yīng),系統(tǒng)
客戶&案例
營銷資訊
關(guān)于我們
微信公眾號
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。