你為什麼看不懂原始碼之Vue 3.0 面面俱到【2】

qqqdu發表於2019-10-11

先嘮會兒嗑

距離上一篇結束已經過去了整整一天,上一篇大部分講讀原始碼前的準備,以及粗略的順了便響應式的流程,戳我看上一篇 這篇主要講,如何讓測試用例跑起來,並且輔助我們解決看不懂的地方。

是騾子是馬,牽出來溜溜

熟悉一個原始碼/工具的方法就是讓它跑起來,更快速的熟悉一個原始碼/工具的方法就是讓它的測試用例跑起來。
先到根目錄安裝下包 npm i

再執行下 reactive 的測試用例
jest packages/reactivity/__tests__/reactive.spec.ts

命令列輸出了讓人賞心悅目的結果。

 PASS  packages/reactivity/__tests__/reactive.spec.ts
  reactivity/reactive
    ✓ Object (6ms)
    ✓ Array (1ms)
    ✓ cloned reactive Array should point to observed values (1ms)
    ✓ nested reactives (5ms)
    ✓ observed value should proxy mutations to original (Object) (1ms)
    ✓ observed value should proxy mutations to original (Array) (1ms)
    ✓ setting a property with an unobserved value should wrap with reactive
    ✓ observing already observed value should return same Proxy (1ms)
    ✓ observing the same value multiple times should return same Proxy
    ✓ should not pollute original object with Proxies
    ✓ unwrap (1ms)
    ✓ non-observable values (1ms)
    ✓ markNonReactive (1ms)
複製程式碼

包羅永珍

為什麼要從測試用例看原始碼呢,因為它就像我們的產品經理,它會告訴我們輸入什麼,預期什麼。它會考慮邊界情況,基本上原始碼難懂的地方都是邊界情況,所以這個階段,我們可以跑用例來理解。

為了支援單個測試用例執行,在 Vscode 商店中安裝 Jest-Runner 外掛,這個外掛可以讓我們更簡單的執行用例和除錯。以下是它的用法。

你為什麼看不懂原始碼之Vue 3.0 面面俱到【2】

我們先選一個測試用例,花幾分鐘,看看jest的基本用法。
這裡我選擇了 reactive.spec.js 用例檔案。

import { reactive, isReactive
  , toRaw, markNonReactive 
} from '../src/reactive'
import { mockWarn } from '@vue/runtime-test'
test('Object', () => {
  const original = { foo: 1 }
  // 用 reactive 包裝 original,original變成了響應式資料
  const observed = reactive(original)
  // 這句很明顯了吧,observed 不等於 original
  expect(observed).not.toBe(original)
  // observed 是響應式資料
  expect(isReactive(observed)).toBe(true)
  // original 不是響應式資料
  expect(isReactive(original)).toBe(false)
  // 通過響應資料 observed 拿到的值與原資料相等
  expect(observed.foo).toBe(1)
  // foo 這個key 值,存在於 observed 中
  expect('foo' in observed).toBe(true)
  // observed 的健集合與原資料相等,toEqual 是深度比較,它會比較值,而非地址
  expect(Object.keys(observed)).toEqual(['foo'])
})
複製程式碼

讀懂 Jest 語法就和讀懂白話文的難度一樣吧。你可以看到 test 的第一個引數是語義化的,基本上能通過這個引數,猜出每個用例想幹什麼,我們將 reactive.spec.js中的引數 列舉出來。

  • Object
  • Array
  • cloned reactive Array should point to observed values
  • nested reactives
  • observed value should proxy mutations to original (Object)
  • observed value should proxy mutations to original (Array)
  • setting a property with an unobserved value should wrap with reactive
  • observing already observed value should return same Proxy
  • observing the same value multiple times should return same Proxy
  • should not pollute original object with Proxies
  • unwrap
  • non-observable values
  • markNonReactive

到此為止,接下來我們再跳入程式碼中,尋找上個文章中留下的問題。

優化!優化!還是TMD優化!

在 reactive.ts 中的 createReactiveObject 方法裡,為什麼要 set 兩次 toProxy.set(target, observed) toRaw.set(observed, target)

首先看這兩個物件是如何消耗的。

// target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }
複製程式碼

很明顯,這兩個Set是用來優化程式碼用的,當 target 存在於時,返回即可。不同的是 toProxy 的key值為 target,toRaw 的 key 值 為 observed。

大膽猜測下,假如 createReactiveObject 執行了兩次,第二次的 target 恰好是 第一次包裝後的 observed。

如果是以上情況,那測試用例肯定存在這種情況。稍微看一眼就是這個case: observing already observed value should return same Proxy

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)
  })
複製程式碼

該用例將包裝好的 observed 再次作為引數傳給了 reactive。 我們把斷點打上。驗證猜想。
當 reactive 執行第二次,到 toRaw 判斷語句的時候便返回了。

你為什麼看不懂原始碼之Vue 3.0 面面俱到【2】

到這裡,為什麼 reactive 內部會 set 兩次的原因已經清晰了:為了優化包裝後的物件再次被傳入的情況,防止多次proxy。
以上,是通過測試用例分析的過程。我們再看看其他用例。

令人困擾的 Ref

上一篇我們是從 Ref 開始閱讀原始碼的,只是大體順了下來,知道了 Ref 物件是怎麼建立的,以及它的 getset 過程。之後,我們看到 reactive.ts,知道它是響應式的核心,並且實現了一個簡單的demo,那 Ref 存在的意義是什麼?

相信我,此時此刻我和你一樣困惑。讓我們開啟 ref.spec.ts 測試用例看看他會告訴我們什麼。

it('should hold a value', () => {
  const a = ref(1)
  expect(a.value).toBe(1)
  a.value = 2
  expect(a.value).toBe(2)
})
複製程式碼

第一個測試用例就給 ref 傳入一個基本型別number。那就該想到,reactive 傳入基本型別會怎麼樣?
讓我們試試!在 reactive.ts 編寫對應測試用例。

你為什麼看不懂原始碼之Vue 3.0 面面俱到【2】
Typescript 的優勢體現出來了,入參只支援物件,不支援基本型別! reactive.ts 核心api 是 Proxy,Proxy 的傳參只能是物件。如果傳基本型別的話,會console

Cannot create proxy with a non-object as target or handler at proxyMethod
所以,ref 是為了使基本型別也能成為響應式資料存在的,讓我們回到第一個測試用例: should hold a value

const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
  // 如果是物件,則用 reactive 方法 包裝 raw
  raw = convert(raw)
  // 返回一個 v 物件,在 取value 值時,呼叫 track 方法,在存 value 值時,呼叫 trigger方法
  const v = {
    [refSymbol]: true,
    get value() {
      track(v, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}
複製程式碼

如果 ref 入參是基本型別的話,這個函式就很容易看懂了,返回值是一個被包裝過的物件。這個物件在 get 時呼叫 track 方法,在 set時,呼叫 trigger 方法走更新 view 層邏輯。因此它是通過這種方式,實現基本型別的資料繫結的。

為了對 ref 有更詳細的認識,我們需要更復雜的的用例。

我擷取了一部分 toRefs 的用例,這部分程式碼不依賴其他模組。

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)
}
複製程式碼

可以看到,這個用例是來測試 toRefs 方法的。
如果用例沒有 toRefs(a),而是

  const { x, y } = reactive({
    x: 1,
    y: 2
  })
複製程式碼

毫無疑問,xy 不是響應式的,二者都是基本型別。我們期望它是響應式資料,所以需要轉化成 Ref 物件。視線再轉回 ref.ts


export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  for (const key in object) {
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    [refSymbol]: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  }
}
複製程式碼

奇怪,前面的 toRefs 可以看懂,遍歷了 object,並用 toProxyRef 包裝後重新賦值。 但 toProxyRef 內部,僅僅用 get set 包裝下,沒有我們可愛的 triggertrack,這是為什麼的?

因為 object 本身就是響應式資料。

其實這裡只需要用存取器包裝成物件,讓基本型別變為引用型別,當執行 expect(x.value).toBe(1) 時,會呼叫 object[key],所以它也會觸發 object 的 get 方法。

同樣的,當執行 x.value = 3 語句時,會呼叫 set 方法,執行 object[key] = newVal 後也會觸發 object 的 set 方法。

其實 toRefs 解決的問題就是,開發者在函式中錯誤的解構 reactive,來返回基本型別。 const { x, y } = = reactive({ x: 1, y: 2 }),這樣會使 x, y 失去響應式,於是官方提出了 toRefs 方案,在函式返回時,將 reactive 轉為 refs,來避免這種情況。

到此為止

如果我們接著探究的話,不得不涉及到其他模組,比如 computed effect......, 而這塊兒又有些龐大,只能後續更新,所以 refreactive 部分探究到此為止。

未完待續

vue-next 的原始碼正在不斷更新中,小夥伴們在看原始碼的過程中,要時不時pull一下,防止原始碼版本滯後呦......

從單測看起的靈感來自: Vue3響應式系統原始碼解析(上),老規矩,先點贊。

我會持續更新,敬請關注。? (千萬別關注我呦)

相關文章