10.5號,國慶佳節(jié),小右男神發(fā)布了vue@3.0." />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運(yùn)營 > Vue3響應(yīng)式系統(tǒng)源碼解析-單測篇

Vue3響應(yīng)式系統(tǒng)源碼解析-單測篇

時(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)生重大不一致,歡迎指出,萬分感謝。

10.5號,國慶佳節(jié),小右男神發(fā)布了vue@3.0.0的alpha版代碼。反正也沒啥事干,最近也在學(xué)TypeScript,正好看看男神的代碼,學(xué)習(xí)一下。

從入口文件packages/vue/index進(jìn)去,初極狹,7行代碼。復(fù)尋數(shù)個(gè)文件,直至runtime-core,豁然開朗。注釋行行,API儼然。算了,編不下去了,總是就是代碼開始變多了。感覺國慶想看完是肯定不可能的,那就挑個(gè)老是面試時(shí)問別人的雙向綁定原理的核心實(shí)現(xiàn)吧。

大家應(yīng)該都知道,Vue3要利用Proxy替換defineProperty來實(shí)現(xiàn)數(shù)據(jù)的響應(yīng)更新,那具體是怎么實(shí)現(xiàn)呢?打開源碼文件目錄,一眼就能知道,核心在于packages/reactivity。

Reactivity

點(diǎn)開它的Readme,通過Google翻譯,我們能明白它的大致意思是:

這個(gè)包會內(nèi)嵌到vue的渲染器中(@vue/runtime-dom)。不過它也可以單獨(dú)發(fā)布且被第三方引用(不依賴vue)。但是呢,你們也別瞎用,如果你們的渲染器是暴露給框架使用者的,它可能已經(jīng)內(nèi)置了一套響應(yīng)機(jī)制,這跟咱們的reactivity是完全的兩套,不一定兼容的(說的就是你,react-dom)。

關(guān)于它的api呢,大家就先看看源碼或者看看types吧。注意:除了 Map, WeakMap, Set and WeakSet 外,內(nèi)置的一些對象是不能被觀測的(例如: Date , RegExp 等)。
唔,單根據(jù)Readme,無法清晰的知道,它具體是怎么樣的。畢竟也是alpha版。那我們還是聽它的,直接擼源碼吧。

一刷源碼,一臉懵逼

從reactivity的入口文件進(jìn)去,發(fā)現(xiàn)它只是暴露了6個(gè)文件內(nèi)的apis。分別是: ref 、reactivecomputed 、effect 、lockoperations 。其中 lockoperations 很簡單, lock 文件內(nèi)部就是兩個(gè)控制鎖開關(guān)變量的方法, operations 內(nèi)部就是對數(shù)據(jù)操作的類型的枚舉。

所以reactivity的重點(diǎn)就在refreactive 、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)部一下奇奇怪怪的操作,繞兩下便迷糊了。

我總結(jié)了下,很大原因是我不知道這幾個(gè)關(guān)鍵的api,到底是要做啥。源碼我不懂、api的含義我也不懂。我們知道,單個(gè)二元一次方程,是求不出解的。

那怎么辦呢?其實(shí)還有一個(gè)方程,那就是單測。從單測開始讀,是一個(gè)極好的閱讀源碼的辦法。不僅能快速知道api的含義跟用法,還能知道很多邊界情況。在閱讀的過程中,還會想,如果是自己的話,會怎么去實(shí)現(xiàn),后續(xù)能加深對源碼的認(rèn)識跟學(xué)習(xí)。

從單測著手

因?yàn)槲倚]了下源碼,所以大致能知道的閱讀順序。當(dāng)然,根據(jù)代碼行數(shù),我們也能估摸個(gè)大致順序。這里我就直接給結(jié)論,建議閱讀順序:reactive -> ref -> effect -> computed -> readonly -> collections

Reactive

reactive 顧名思義,響應(yīng)式,意味著 reactive 數(shù)據(jù)是響應(yīng)式數(shù)據(jù),從名字上就說明了它是本庫的核心。那我們先來看看它有什么樣的能力。

第一個(gè)單測:

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è)啥,也不知道,以后再說。

第二個(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 。

第三個(gè)單測,沒啥好講的,第四個(gè)單測,測試嵌套對象,在我第二個(gè)單測的補(bǔ)充中已經(jīng)覆蓋了。

第五個(gè)單測:

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)})
在這個(gè)單測中,我們終于見識到“響應(yīng)式**”。通過 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ù)。

第六個(gè)單測

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ù)組了。

第七個(gè)單測:

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í)添加刪除了。

第八、九個(gè)單測

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é)果。

第十個(gè)單測

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 做反向映射。

第十一個(gè)單測,不貼代碼了,本單測列舉了不可成為響應(yīng)數(shù)據(jù)的數(shù)據(jù)類型,即JS五種基本數(shù)據(jù)類型+ Symbol (經(jīng)本人測試,函數(shù)也不支持)。而對于內(nèi)置一些的特殊類型,如 Promise 、RegExp 、Date ,這三個(gè)類型的數(shù)據(jù)傳遞給 reactive 時(shí)不會報(bào)錯(cuò),會直接返回原始數(shù)據(jù)。

最后一個(gè)單測

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ù)就跟影子一樣,對任何一方的任何操作都能同步到對方身上。

但這...好像沒什么厲害之處。但從單測的表現(xiàn)來說,就是基于Proxy,做了一些邊界跟嵌套上的處理。那這就引出了一個(gè)非常關(guān)鍵的問題:在vue@3中,它是如何通知視圖更新的?或者說,當(dāng)響應(yīng)數(shù)據(jù)變更時(shí),它是如何通知它的使用方,要做一些操作的?這些行為肯定是封裝在Proxy的set/get等各類handler中。但目前還不知道,只能先繼續(xù)往下看其他單測啦。

由于最開始,我們就知道了, reactive 的返回值是個(gè) UnwrapNestedRefs 類型,乍一看是一種特殊的 Ref 類型,那咱們就繼續(xù)看看 ref 。(實(shí)際上這個(gè)UnwrapNestedRefs是為了獲取嵌套Ref的泛型的類型,記住這個(gè)Unwrap是一個(gè)動詞,這有點(diǎn)兒繞,以后講源碼解析時(shí)再闡述)

Ref

那先看ref的第一個(gè)單測:

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。

再看第二個(gè)單測:

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值。

也就是說,如果向effect傳遞一個(gè)方法,會立即執(zhí)行一次,每當(dāng)其內(nèi)部依賴的ref數(shù)據(jù)發(fā)生變更時(shí),會重新執(zhí)行。這就解開了之前閱讀 reactive 時(shí)的疑惑:當(dāng)響應(yīng)數(shù)據(jù)變化時(shí),如何通知其使用方?很明顯,就是通過effect。每當(dāng) reactive 數(shù)據(jù)變化時(shí),觸發(fā)依賴其的effect方法執(zhí)行。

感覺這也不難實(shí)現(xiàn),那如果是我的話,應(yīng)該會這么做:

  1. 首先需要維持一個(gè)effects的二維Map;
  2. effect 函數(shù)傳遞一個(gè)響應(yīng)函數(shù);
  3. 這個(gè)響應(yīng)函數(shù)會立即執(zhí)行一次,若其內(nèi)部引用了響應(yīng)數(shù)據(jù),由于這些數(shù)據(jù)已經(jīng)被我通過Proxy劫持了set/get,所以可據(jù)此收集此函數(shù)的依賴,更新effects二維Map
  4. 后續(xù)任意的ref數(shù)據(jù)變更(觸發(fā)set)時(shí),檢查二維Map,找到相應(yīng)的effect,觸發(fā)他們執(zhí)行。
但有一個(gè)麻煩之處是, ref 函數(shù)也支持非對象數(shù)據(jù),而Proxy僅支持對象。所以在本庫 reactivity 中針對非對象數(shù)據(jù)會進(jìn)行一層對象化的包裝,再通過.value去取值。

再看第三個(gè)單測:

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è)好奇:

  1. 如果再嵌套一層呢?
  2. 因?yàn)樵紨?shù)據(jù)是個(gè)對象,如果我直接修改原始數(shù)據(jù),會同步到代理數(shù)據(jù)嗎?
  3. 直接修改原數(shù)據(jù),會觸發(fā)effect嗎?
于是我假使1.可以嵌套,2. 會同步,3.不會觸發(fā)effect。改造了下單測,變成了:

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í)最初是我試出來的,只是為了寫文章順暢寫的如我所料):

  1. 無論對象如何嵌套,修改代理數(shù)據(jù),都能觸發(fā)依賴其的effect
  2. 修改原始數(shù)據(jù),代理數(shù)據(jù)get新數(shù)據(jù)時(shí)能同步,但不會觸發(fā)依賴其代理數(shù)據(jù)的effect。
所以我們能得出一個(gè)結(jié)論:對于 Ref 數(shù)據(jù)的更新,會觸發(fā)依賴其的effect的執(zhí)行。Reactive 數(shù)據(jù)呢?我們繼續(xù)往下看。

第四個(gè)單測

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),我們可以清晰的看到。

到這我們其實(shí)能理解 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í)再具體闡述吧。

另外,本單測解開了上個(gè)單測中我們的疑問,修改 Reactive 數(shù)據(jù),也會觸發(fā)effect的更新。

第五個(gè)單測

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一次即可。

不過這個(gè)能力在小右10月5號的發(fā)布的第一個(gè)版本是有欠缺的,它不能推導(dǎo)嵌套超過9層的數(shù)據(jù)。這個(gè)commit解決了這個(gè)問題,對TS類型推導(dǎo)有興趣的同學(xué)可以看下。

第六個(gè)單測

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í)行。

最后一個(gè)單測

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ù)單測來看, toRefsref 的區(qū)別就是, ref 會將傳入的數(shù)據(jù)變成 Ref 類型,而 toRefs 要求傳入的數(shù)據(jù)必須是object,然后將此對象的第一層數(shù)據(jù)轉(zhuǎn)為 Ref 類型。也不知道它能干什么用,知道效果是怎么樣就行。

至此ref的單測看完了,大致可以感受到ref最重要的目的就是,實(shí)現(xiàn)非對象數(shù)據(jù)的劫持。其他的話,似乎沒有其他特殊的用處。實(shí)際上在effect的測試文件中,目前也只測試了 reactive 數(shù)據(jù)觸發(fā)effect方法。

那下面我們看看effect的測試文件。

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)測試代碼。

基本能力

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)})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)})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)})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)})

ReactiveEffectOptions

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}effect 的邏輯雖然很多,但核心概念還是好理解的,需要關(guān)注的是內(nèi)部一些特殊的優(yōu)化,將來閱讀源碼時(shí)需要重點(diǎn)看看。接下來還有個(gè) computed 我們接觸了但還沒閱讀。

Computed

計(jì)算屬性。這個(gè)寫過vue的同學(xué),應(yīng)該的都能知道是什么意思。我們看看在 reactivity 中它具體如何。

第一個(gè)單測

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ù)。


查看TS Type,果然 ComputedRef 繼承于 Ref ,相比 Ref 多了一個(gè)只讀的 effect 屬性,類型是 ReactiveEffect 。那能猜到,此處的effect屬性的值應(yīng)該就是我們傳給 computed 的計(jì)算函數(shù),再被 effect 函數(shù)執(zhí)行后返回的結(jié)果。另外其 value 是只讀的,說明 computed 的返回結(jié)果的value值是只讀的。

第二個(gè)單測

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 很多特性:

第一個(gè)單測中,我們猜想 ComputedRef 的effect屬性,是通過向 effect 方法傳遞 getter 函數(shù)生成的監(jiān)聽函數(shù)。但是在 effect 單測中,一旦依賴數(shù)據(jù)變更,這個(gè)監(jiān)聽函數(shù)就會立即執(zhí)行,這就跟此處 computed 的表現(xiàn)不一致了。這其中一定有貓膩!

在上一小節(jié) Effect 的最后,我們發(fā)現(xiàn) effect 函數(shù)第二個(gè)參數(shù)是個(gè)配置項(xiàng),而其中有個(gè)配置就叫computed,在單測中也沒覆蓋到。估計(jì)就是這個(gè)配置項(xiàng),實(shí)現(xiàn)了此處計(jì)算數(shù)據(jù)的延遲計(jì)算。

第三個(gè)單測

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 依賴于 cValuecValue 依賴于 value ,只要 value 變更,不管 cValue 有沒有改變,都會重新觸發(fā) reactiveEffect 。感覺這里可以優(yōu)化下,有興趣的同學(xué)可以去提PR。

第四個(gè)單測

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è)單測說明了 computedgetter 函數(shù)可以依賴于另外的 computed 數(shù)據(jù)。

第五第六個(gè)單測屬于變著花兒的使用 computed 。傳達(dá)的概念就是:使用 computed 數(shù)據(jù)跟使用正常的響應(yīng)數(shù)據(jù)差不多,都能正確的觸發(fā)監(jiān)聽函數(shù)的執(zhí)行。

第七個(gè)單測

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)更新。

最后兩個(gè)單測

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)的概念,還剩下 readonlycollectionsreadonly 單測文件特別多,但實(shí)際上概念很簡單的,就是 reactive 的只讀版本。 collections 單測是為了覆蓋 Map 、SetWeakMap 、WeakSet 的響應(yīng)更新的,暫時(shí)不看的問題應(yīng)該也不大。

總結(jié)

梳理完以后,我們應(yīng)該對內(nèi)部的主要api有了清晰的認(rèn)識,我們再總結(jié)復(fù)習(xí)一下:

reactive: 本庫的核心方法,傳遞一個(gè)object類型的原始數(shù)據(jù),通過Proxy,返回一個(gè)代理數(shù)據(jù)。在這過程中,劫持了原始數(shù)據(jù)的任何讀寫操作。進(jìn)而實(shí)現(xiàn)改變代理數(shù)據(jù)時(shí),能觸發(fā)依賴其的監(jiān)聽函數(shù)effect。

ref:這是最影響代碼閱讀的一個(gè)文件(粗看代碼很容易搞暈它跟reactive的關(guān)系),但要想真正明白它,又需要仔細(xì)閱讀代碼。建議在理清其他邏輯前,千萬別管它....當(dāng)它不存在。只要知道,這個(gè)文件最重要的作用就是提供了一套 Ref 類型。

effect:接受一個(gè)函數(shù),返回一個(gè)新的監(jiān)聽函數(shù) reactiveEffect 。若監(jiān)聽函數(shù)內(nèi)部依賴了reactive數(shù)據(jù),當(dāng)這些數(shù)據(jù)變更時(shí)會觸發(fā)監(jiān)聽函數(shù)。

computed: 計(jì)算數(shù)據(jù),接受一個(gè)getter函數(shù)或者包含get/set行為的對象,返回一個(gè)響應(yīng)式的數(shù)據(jù)。它若有變更,也會觸發(fā)reactiveEffect。

最后畫了張大致的圖,方便記憶回顧。

不過這張圖,我還不保證對,因?yàn)樵创a我還沒好好擼完。這周我再抽時(shí)間,寫篇真正的源碼解析。

關(guān)鍵詞:響應(yīng),系統(tǒng)

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉