距離上一篇結束已通過去了整整一天,上一篇大部分講讀源碼前的準備,以及粗略的順了便響應式的流程,戳我看上一篇 這篇主要講,如何讓測試用例跑起來,而且輔助咱們解決看不懂的地方。vue
熟悉一個源碼/工具的方法就是讓它跑起來,更快速的熟悉一個源碼/工具的方法就是讓它的測試用例跑起來。
先到根目錄安裝下包 npm i
react
再運行下 reactive
的測試用例
jest packages/reactivity/__tests__/reactive.spec.ts
typescript
命令行輸出了讓人賞心悅目的結果。npm
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)
複製代碼
爲何要從測試用例看源碼呢,由於它就像咱們的產品經理,它會告訴咱們輸入什麼,預期什麼。它會考慮邊界狀況,基本上源碼難懂的地方都是邊界狀況,因此這個階段,咱們能夠跑用例來理解。api
爲了支持單個測試用例運行,在 Vscode 商店中安裝 Jest-Runner
插件,這個插件可讓咱們更簡單的運行用例和調試。如下是它的用法。bash
咱們先選一個測試用例,花幾分鐘,看看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
中的參數 列舉出來。工具
到此爲止,接下來咱們再跳入代碼中,尋找上個文章中留下的問題。post
在 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 判斷語句的時候便返回了。
上一篇咱們是從 Ref
開始閱讀源碼的,只是大致順了下來,知道了 Ref
對象是怎麼建立的,以及它的 get
、set
過程。以後,咱們看到 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
編寫對應測試用例。
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
})
複製代碼
毫無疑問,x
和 y
不是響應式的,兩者都是基本類型。咱們指望它是響應式數據,因此須要轉化成 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
包裝下,沒有咱們可愛的 trigger
和 track
,這是爲何的?
由於 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
......, 而這塊兒又有些龐大,只能後續更新,因此 ref
和 reactive
部分探究到此爲止。
vue-next 的源碼正在不斷更新中,小夥伴們在看源碼的過程當中,要時不時pull一下,防止源碼版本滯後呦......
從單測看起的靈感來自: Vue3響應式系統源碼解析(上),老規矩,先點贊。
我會持續更新,敬請關注。😁 (千萬別關注我呦)