注意:在我寫文章的時候,可能代碼已有變動。在您讀文章的時候,代碼更有可能變動,若有不一致且有會對源碼實現或理解產生重大不一致,歡迎指出,萬分感謝。
javascript
10.5號,國慶佳節,小右男神發佈了vue@3.0.0的alpha版代碼。反正也沒啥事幹,最近也在學TypeScript,正好看看男神的代碼,學習一下。vue
從入口文件packages/vue/index進去,初極狹,7行代碼。復尋數個文件,直至runtime-core,豁然開朗。註釋行行,API儼然。算了,編不下去了,總之就是代碼開始變多了。感受國慶想看完是確定不可能的,那就挑個總是面試時問別人的雙向綁定原理的核心實現吧。java
你們應該都知道,Vue3要利用Proxy替換defineProperty來實現數據的響應更新,那具體是怎麼實現呢?打開源碼文件目錄,一眼就能知道,核心在於packages/reactivity。react
點開它的Readme,經過Google翻譯,咱們能明白它的大體意思是:git
這個包會內嵌到vue的渲染器中(@vue/runtime-dom)。不過它也能夠單獨發佈且被第三方引用(不依賴vue)。可是呢,大家也別瞎用,若是大家的渲染器是暴露給框架使用者的,它可能已經內置了一套響應機制,這跟我們的reactivity是徹底的兩套,不必定兼容的(說的就是你,react-dom)。github
關於它的api呢,你們就先看看源碼或者看看types吧。注意:除了
Map
,WeakMap
,Set
andWeakSet
外,內置的一些對象是不能被觀測的(例如: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
數據是響應式數據,從名字上就說明了它是本庫的核心。那咱們先來看看它有什麼樣的能力。
第一個單測:
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
的聲明:
說明 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的第一個單測:
it('should hold a value', () => {
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
複製代碼
那咱們先看下 ref
函數的聲明,傳遞任何數據,能返回一個 Ref
數據。
而 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方法執行。
感受這也不難實現,那若是是個人話,應該會這麼作:
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。改造了下單測,變成了:
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)
})
複製代碼
結果如我所料(其實最初是我試出來的,只是爲了寫文章順暢寫的如我所料):
因此咱們能得出一個結論:**對於 ****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的類型推導,咱們能夠清晰的看到。
到這咱們其實能理解 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跟類型推導,咱們也能看出來:
那若是我多套幾層呢,好比這樣:
const a = {
b: ref(0),
d: {
b: ref(0),
d: ref({
b: 0,
d: {
b: ref(0)
}
})
}
}
const c = ref(a)
複製代碼
反正就是套來套去,一下套一下又不套,根據TS類型推導,咱們發現這種狀況也毫無問題,只要最開始.value一次便可。
不過這個能力在小右10月5號的發佈的第一個版本是有欠缺的,它不能推導嵌套超過9層的數據。這個commit解決了這個問題,對TS類型推導有興趣的同窗能夠看下。
第六個單測
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
的單測量卻不少,有39個用例,600多行代碼,不少邊界狀況的考慮。因此針對effect,我就不一個個列舉了。我先幫你們看一遍,而後總結分紅幾個小點,直接總結關鍵結論,有必要的話,再貼上相應測試代碼。
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.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,終止監聽函數繼續監聽。(感受能夠再加個 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)
})
複製代碼
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)
})
複製代碼
effect
還能接受第二參數 ReactiveEffectOptions
,參數以下:
export interface ReactiveEffectOptions {
lazy?: boolean
computed?: boolean
scheduler?: (run: Function) => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
onStop?: () => void
}
複製代碼
computed
有關係,先放着。stop
終止監聽函數時觸發的事件。effect
的邏輯雖然不少,但核心概念仍是好理解的,須要關注的是內部一些特殊的優化,未來閱讀源碼時須要重點看看。接下來還有個 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
數據。
查看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。
最後畫了張大體的圖,方便記憶回顧。
不過這張圖,我還不保證對,由於源碼我還沒好好擼完。這周我再抽時間,寫篇真正的源碼解析。
本文做者:螞蟻保險-體驗技術組-阿相
掘金地址:相學長