Vue3響應式系統源碼解析-單測篇

注意:在我寫文章的時候,可能代碼已有變動。在您讀文章的時候,代碼更有可能變動,若有不一致且有會對源碼實現或理解產生重大不一致,歡迎指出,萬分感謝。
javascript

10.5號,國慶佳節,小右男神發佈了vue@3.0.0的alpha版代碼。反正也沒啥事幹,最近也在學TypeScript,正好看看男神的代碼,學習一下。vue

從入口文件packages/vue/index進去,初極狹,7行代碼。復尋數個文件,直至runtime-core,豁然開朗。註釋行行,API儼然。算了,編不下去了,總之就是代碼開始變多了。感受國慶想看完是確定不可能的,那就挑個總是面試時問別人的雙向綁定原理的核心實現吧。java

你們應該都知道,Vue3要利用Proxy替換defineProperty來實現數據的響應更新,那具體是怎麼實現呢?打開源碼文件目錄,一眼就能知道,核心在於packages/reactivity。react

Reactivity

點開它的Readme,經過Google翻譯,咱們能明白它的大體意思是:git

這個包會內嵌到vue的渲染器中(@vue/runtime-dom)。不過它也能夠單獨發佈且被第三方引用(不依賴vue)。可是呢,大家也別瞎用,若是大家的渲染器是暴露給框架使用者的,它可能已經內置了一套響應機制,這跟我們的reactivity是徹底的兩套,不必定兼容的(說的就是你,react-dom)。github

關於它的api呢,你們就先看看源碼或者看看types吧。注意:除了 Map , WeakMap , Set and WeakSet 外,內置的一些對象是不能被觀測的(例如: 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 顧名思義,響應式,意味着 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 的聲明:

image.png

說明 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

那先看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 數據。

image.png
image.png

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方法執行。

感受這也不難實現,那若是是個人話,應該會這麼作:

  1. 首先須要維持一個effects的二維Map;
  2. effect 函數傳遞一個響應函數;
  3. 這個響應函數會當即執行一次,若其內部引用了響應數據,因爲這些數據已經被我經過Proxy劫持了set/get,因此可據此收集此函數的依賴,更新effects二維Map
  4. 後續任意的ref數據變動(觸發set)時,檢查二維Map,找到相應的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嗎?

因而我假使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)
  })
複製代碼

結果如我所料(其實最初是我試出來的,只是爲了寫文章順暢寫的如我所料):

  1. 不管對象如何嵌套,修改代理數據,都能觸發依賴其的effect
  2. 修改原始數據,代理數據get新數據時能同步,但不會觸發依賴其代理數據的effect。

因此咱們能得出一個結論:**對於 ****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的類型推導,咱們能夠清晰的看到。

image.png

到這咱們其實能理解 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跟類型推導,咱們也能看出來:

image.png
image.png

那若是我多套幾層呢,好比這樣:

const a = {
  b: ref(0),
  d: {
    b: ref(0),
    d: ref({
      b: 0,
      d: {
        b: ref(0)
      }
    })
  }
}

const c = ref(a)
複製代碼

反正就是套來套去,一下套一下又不套,根據TS類型推導,咱們發現這種狀況也毫無問題,只要最開始.value一次便可。

image.png

不過這個能力在小右10月5號的發佈的第一個版本是有欠缺的,它不能推導嵌套超過9層的數據。這個commit解決了這個問題,對TS類型推導有興趣的同窗能夠看下。

image.png

第六個單測

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 的行爲其實從上述的測試文件中,咱們已經能明白了。主要就是能夠監聽響應式數據的變化,觸發監聽函數的執行。事情描述雖然簡單,但 effect 的單測量卻不少,有39個用例,600多行代碼,不少邊界狀況的考慮。因此針對effect,我就不一個個列舉了。我先幫你們看一遍,而後總結分紅幾個小點,直接總結關鍵結論,有必要的話,再貼上相應測試代碼。

基本能力

  • 傳遞給effect的方法,會當即執行一次。(除非第二個參數傳遞了{ lazy: true },這得從源碼看,單測沒覆蓋,有興趣的同窗,能夠去提PR)。
  • 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值,如 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)
})
複製代碼
  • 對響應數據的原始數據的操做,不會觸發監聽函數
  • 一個監聽函數內容許引入另一個監聽函數。
  • 每次effect執行返回的都是全新的監聽函數,即便傳遞的相同的函數。
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)
})
複製代碼
  • **若是effect內部的依賴是有邏輯分支的,監聽函數每次執行之後會從新更新依賴。**以下所示:當 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)
})
複製代碼

ReactiveEffectOptions

effect 還能接受第二參數 ReactiveEffectOptions ,參數以下:

export interface ReactiveEffectOptions {
  lazy?: boolean
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}
複製代碼
  • lazy: 延遲計算,爲true時候,傳入的effect不會當即執行。
  • computed:單測中沒體現,不知道幹啥用,看名字多是跟 computed 有關係,先放着。
  • scheduler:調度器函數,接受的入參run便是傳給effect的函數,若是傳了scheduler,則可經過其調用監聽函數。
  • onStop:經過 stop 終止監聽函數時觸發的事件。
  • onTrack僅供調試使用。在收集依賴(get階段)的過程當中觸發。
  • onTrigger僅供調試使用。在觸發更新後執行監聽函數以前觸發。

effect 的邏輯雖然不少,但核心概念仍是好理解的,須要關注的是內部一些特殊的優化,未來閱讀源碼時須要重點看看。接下來還有個 computed 咱們接觸了但還沒閱讀。

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 數據。

image.png

查看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。

最後畫了張大體的圖,方便記憶回顧。

image.png

不過這張圖,我還不保證對,由於源碼我還沒好好擼完。這周我再抽時間,寫篇真正的源碼解析。


本文做者:螞蟻保險-體驗技術組-阿相

掘金地址:相學長

相關文章
相關標籤/搜索