爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀javascript
在上一章中,咱們介紹了ref
,若是仔細看過,想必對ref
應該已經瞭如指掌了。若是尚未,或着忘記了....能夠先回顧一下上篇文章。html
閱讀本篇文章須要的前置知識有:vue
reactive
這個文件代碼其實很少,100 來行,不少邏輯實際上是在handlers
跟effect
中。咱們先看這個文件的引入:java
import {
isObject, // 判斷是不是對象
toTypeString // 獲取數據的類型名稱
} from '@vue/shared'
// 此處的handles最終會傳遞給Proxy(target, handle)的第二個參數
import {
mutableHandlers, // 可變數據代理處理
readonlyHandlers // 只讀(不可變)數據代理處理
} from './baseHandlers'
// collections 指 Set, Map, WeakMap, WeakSet
import {
mutableCollectionHandlers, // 可變集合數據代理處理
readonlyCollectionHandlers // 只讀集合數據代理處理
} from './collectionHandlers'
// 上篇文章中說了半天的泛型類型
import { UnwrapRef } from './ref'
// 看過單測篇的話,應該知道這個是被effect執行後返回的監聽函數的類型
import { ReactiveEffect } from './effect'
複製代碼
因此不用怕,不少只是引了簡單的工具方法跟類型,真正跟外部函數有關聯的就是幾個handlers
。react
再來看類型的聲明跟變量的聲明,先看註釋不少的targetMap
。typescript
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
// 翻譯自上述英文:利用WeakMap是爲了更好的減小內存開銷。
export const targetMap = new WeakMap<any, KeyToDepMap>()
複製代碼
traget
的意思就是Proxy(target, handle)
函數的第一個入參,也就是咱們想轉成響應式數據的原始數據。但這個KeyToDepMap
其實看不明白具體是怎麼樣的映射。先放着,等到咱們真正使用它時,再來看。api
繼續往下看,是一堆常量的聲明。數組
// raw這個單詞在ref篇咱們見過,它在這個庫的含義是,原始數據
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
// 集合類型
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
// 用於正則判斷是否符合可觀察數據,object + array + collectionTypes
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
複製代碼
若是讀過單測篇(reactive 的第 八、九、10 個單測),可能會記得以前說過,內部須要兩個WeakMap
來實現原始數據跟響應數據的雙向映射。明顯的rawToReactive
跟reactiveToRaw
就是這兩個WeakMap
。rawToReadonly
跟readonlyToRaw
顧名思義的就是映射原始數據跟只讀的響應數據的兩個WeakMap
。app
readonlyValues
跟nonReactiveValues
根據註釋以及以前單測篇的記憶,多是跟markNonReactive
跟markReadonly
(這個單測篇沒講到)有關。猜想是用來存儲用這兩個 api 構建的數據,具體也能夠後面再看。dom
collectionTypes
跟observableValueRE
看註釋便可。
在真正看reactive
以前,咱們把本文件內部的一些工具方案先過一遍,這樣看源碼時就不會東跳西跳比較亂。這部分比較簡單,簡單瞄兩眼就行了。
// 數據是否可觀察
const canObserve = (value: any): boolean => {
return (
// 整個vue3庫都沒搜到_isVue的邏輯,猜想是vue組件,不影響本庫閱讀
!value._isVue &&
// 虛擬dom的節點不可觀察
!value._isVNode &&
// 屬於可觀察的數據類型
observableValueRE.test(toTypeString(value)) &&
// 該集合中存儲的數據不可觀察
!nonReactiveValues.has(value)
)
}
// 若是reactiveToRaw或readonlyToRaw中存在該數據了,說明就是響應式數據
export function isReactive(value: any): boolean {
return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}
// 判斷是不是隻讀的響應式數據
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value)
}
// 將響應式數據轉爲原始數據,若是不是響應數據,則返回源數據
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
// 傳遞數據,將其添加到只讀數據集合中
// 注意readonlyValues是個WeakSet,利用set的元素惟一性,能夠避免重複添加
export function markReadonly<T>(value: T): T {
readonlyValues.add(value)
return value
}
// 傳遞數據,將其添加至不可響應數據集合中
export function markNonReactive<T>(value: T): T {
nonReactiveValues.add(value)
return value
}
複製代碼
上述的代碼都是佐料,下面看本文件的核心代碼,首先看reactive
跟readonly
函數
// 函數類型聲明,接受一個對象,返回不會深度嵌套的Ref數據
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 函數實現
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 若是傳遞的是一個只讀響應式數據,則直接返回,這裏其實能夠直接用isReadonly
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
// 若是是被用戶標記的只讀數據,那經過readonly函數去封裝
if (readonlyValues.has(target)) {
return readonly(target)
}
// 到這一步的target,能夠保證爲非只讀數據
// 經過該方法,建立響應式對象數據
return createReactiveObject(
target, // 原始數據
rawToReactive, // 原始數據 -> 響應式數據映射
reactiveToRaw, // 響應式數據 -> 原始數據映射
mutableHandlers, // 可變數據的代理劫持方法
mutableCollectionHandlers // 可變集合數據的代理劫持方法
)
}
// 函數聲明+實現,接受一個對象,返回一個只讀的響應式數據。
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
// value is a mutable observable, retrieve its original and return
// a readonly version.
// 若是自己是響應式數據,獲取其原始數據,並將target入參賦值爲原始數據
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
// 建立響應式數據
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
複製代碼
兩個方法代碼其實很簡單,主要邏輯都封裝到了createReactiveObject
,兩個方法的主要做用是:
createReactiveObject
相應地的代理數據與響應式數據的雙向映射 map。reactive
會作readonly
的相關校驗,反之readonly
方法也是。下面繼續看:
function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
// 不是一個對象,直接返回原始數據,在開發環境下會打警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 經過原始數據 -> 響應數據的映射,獲取響應數據
let observed = toProxy.get(target)
// target already has corresponding Proxy
// 若是原始數據已是響應式數據,則直接返回此響應數據
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 若是原始數據自己就是個響應數據了,直接返回自身
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
// 若是是不可觀察的對象,則直接返回原對象
if (!canObserve(target)) {
return target
}
// 集合數據與(對象/數組) 兩種數據的代理處理方式不一樣。
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 聲明一個代理對象,也便是響應式數據
observed = new Proxy(target, handlers)
// 設置好原始數據與響應式數據的雙向映射
toProxy.set(target, observed)
toRaw.set(observed, target)
// 在這裏用到了targetMap,可是它的value值存放什麼咱們依舊不知道
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
複製代碼
咱們能夠看到一些細節:
reactive
文件其實很是通俗易懂,看完之後,咱們心中只有 2 個問題:
baseHandlers
,collectionHandlers
的具體實現以及爲何要區分?targetMap
究竟是啥?固然咱們知道handlers
確定是作依賴收集跟響應觸發的。那咱們就先看着兩個文件。
打開此文件,一樣先看外部引用:
// 這些咱們已經瞭解了
import { reactive, readonly, toRaw } from './reactive'
import { isRef } from './ref'
// 這些就是些工具方法,hasOwn 意爲對象是否擁有某數據
import { isObject, hasOwn, isSymbol } from '@vue/shared'
// 這裏定義了操做數據的行爲枚舉
import { OperationTypes } from './operations'
// LOCKED:global immutability lock
// 一個全局用來判斷是否數據是不可變的開關
import { LOCKED } from './lock'
// 收集依賴跟觸發監聽函數的兩個方法
import { track, trigger } from './effect'
複製代碼
只有track
跟trigger
的內部實現咱們不知道,其餘的要麼已經瞭解了,要麼點開看看一眼就明白。
而後是一個表明 JS 內部語言行爲的描述符的集合,不明白的能夠看相應MDN。具體怎麼使用能夠後面再看。
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => Symbol[key])
.filter(key => typeof key === 'symbol')
)
複製代碼
而後會發現下面就百來行代碼,咱們找到reactive
中引用的mutableHandlers
、readonlyHandlers
。咱們先看簡單的mutableHandlers
:
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
複製代碼
這是一個ProxyHandle
,關於Proxy
若是忘記了,記得再看一遍MDN。
而後終於到了整個響應式系統最關鍵的地方了,這五個traps
:get
,set
,deleteProperty
,has
,ownKeys
。固然Proxy
能實現的trap
並不只是這五個。其中defineProperty
跟getOwnPropertyDescriptor
兩個trap
不涉及響應式,不須要劫持。還有一個enumerate
已經被廢棄。enumerate
本來會劫持for-in
的操做的,那你會想,那這個廢棄了,咱們的for-in
怎麼辦?放心,它仍是走到ownKeys
這個trap
,進而觸發咱們的監聽函數的。
說遠了,回到代碼中,咱們從負責收集依賴的get
看。這個trap
是經過createGetter
函數生成,那咱們來看看它。
createGetter
接受一個入參:isReadonly
。那天然在readonlyHandlers
中就是傳true
。
// 入參只有一個是否只讀
function createGetter(isReadonly: boolean) {
// 關於proxy的get,請閱讀:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
// receiver便是被建立出來的代理對象
return function get(target: any, key: string | symbol, receiver: any) {
// 若是還不瞭解Reflect,建議先閱讀它的文檔:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
// 獲取原始數據的相應值
const res = Reflect.get(target, key, receiver)
// 若是是js的內置方法,不作依賴收集
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 若是是Ref類型數據,說明已經被收集過依賴,不作依賴收集,直接返回其value值。
if (isRef(res)) {
return res.value
}
// 收集依賴
track(target, OperationTypes.GET, key)
// 經過get獲取的值不是對象的話,則直接返回便可
// 不然,根據isReadyonly返回響應數據
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
複製代碼
大體看下來會發現,get
的方法的每一個表達式其實都比較簡單,不過卻好像都有點兒懵。
問題 1: 爲何要經過Reflect
, 而不是直接target[key]
?
確實,target[key]
好像就能實現效果了,爲何要用Reflect
,還要傳個receiver
呢?緣由在於原始數據的get
並無你們想的這麼簡單,好比這種狀況:
const targetObj = {
get a() {
return this
}
}
const proxyObj = reactive(targetObj)
複製代碼
這個時候,proxyObj.a
在你想象中應該是proxyObj
仍是targetObj
呢?我以爲合理來講應該是proxyObj
。但a
又不是一個方法,無法直接call/apply
。要實現也能夠,比較繞,約等於實現了Reflect
的 polyfill。因此感謝 ES6,利用Reflect
,可方便的把現有操做行爲原模原樣地反射到目標對象上,又保證真實的做用域(經過第三個參數receiver
)。這個receiver
便是生成的代理對象,在上述例子中便是proxyObj
。
問題 2: 爲何內置方法不須要收集依賴?
若是一個監聽函數是這樣的:
const origin = {
a() {}
}
const observed = reactive(origin)
effect(() => {
console.log(observed.a.toString())
})
複製代碼
很明顯,當origin.a
變化時,observed.a.toString()
也是應該會變的,那爲何不用監聽了呢?很簡單,由於已經走到了observed.a.toString()
已經走了一次get
的 trap,不必重複收集依賴。故而相似的內置方法,直接 return。
問題 3: 爲何屬性值爲對象時,須要再用reactive|readonly
執行?
註釋中寫了:
need to lazy access readonly and reactive here to avoid circular dependency
翻譯成普通話是,須要延遲地使用reactive|readonly
來避免循環依賴。這話須要品,細細品,品了一下子之後終於品懂了。
由於因爲Proxy
這玩意兒吧,它的trap
其實只能劫持對象的第一層訪問與更新。若是是嵌套對象,實際上是劫持不了的。那咱們就有了兩種方法:
方法一:當經過reactive|readonly
轉化原始對象時,一層一層的遞歸解套,若是是對象,就再用reactive
執行、而後走ProxyHandle
。之後訪問這些嵌套屬性時,天然也會走到 trap。但這樣有個大問題,若是對象是循環引用的呢?那必然是要有個邏輯判斷,若是發現屬性值是自身則不遞歸了。那若是是半路循環引用的呢?好比這樣:
const a = {
b: {
c: a
}
}
const A = {
B: {
C: a
}
}
複製代碼
想一想都頭大吧。
方法二:也便是源碼中的方法,轉化原始對象時,不遞歸。後續走到get
的 trap 時,若是發現屬性值是個對象,再繼續轉化、劫持。也就是註釋中所講到的lazy
。利用這個辦法,天然就能夠避免循環引用了。另外還有個顯而易見的好處是,能夠優化性能。
除了這個三個問題外,還有一個小細節:
if (isRef(res)) {
return res.value
}
複製代碼
若是是Ref
類型的數據,則直接返回 value 值。由於在ref
函數中,已經作了相關的依賴跟蹤邏輯。另外,若是看過單測篇跟 ref 篇,咱們知道就是此處代碼實現了這樣的能力:向reacitive
函數傳遞一個嵌套的Ref
類型數據,可返回一個遞歸解套了Ref
類型的響應式數據。reactive
函數的返回類型爲UnwrapNestedRefs
歸功於此。
不過切記:向reactive
傳一個純粹的Ref
類型數據,是不會解套的,它只解套被嵌套着的Ref
數據。示例以下:
reactive(ref(4)) // = ref(4);
reactive({ a: ref(4) }) // = { a: 4 }
複製代碼
那到此爲止,除了track
是外部引入的用來收集依賴的方法外(後面再看),get
已經摸透了。
下面看set
。
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
// 若是value是響應式數據,則返回其映射的源數據
value = toRaw(value)
// 獲取舊值
const oldValue = target[key]
// 若是舊值是Ref數據,但新值不是,那更新舊的值的value屬性值,返回更新成功
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 代理對象中,是否是真的有這個key,沒有說明操做是新增
const hadKey = hasOwn(target, key)
// 將本次設置行爲,反射到原始對象上
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 若是是原始數據原型鏈上的數據操做,不作任何觸發監聽函數的行爲。
if (target === toRaw(receiver)) {
// istanbul 是個單測覆蓋率工具
/* istanbul ignore else */
if (__DEV__) {
// 開發環境下,會傳給trigger一個擴展數據,包含了新舊值。明顯的是便於開發環境下作一些調試。
const extraInfo = { oldValue, newValue: value }
// 若是不存在key時,說明是新增屬性,操做類型爲ADD
// 存在key,則說明爲更新操做,當新值與舊值不相等時,纔是真正的更新,進而觸發trigger
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
// 同上述邏輯,只是少了extraInfo
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
複製代碼
set
跟get
同樣,每句表達式都很清晰,但咱們依舊存在疑問。
問題 1:isRef(oldValue) && !isRef(value)
這段是什麼邏輯?
// 若是舊值是 Ref 數據,但新值不是,那更新舊的值的 value 屬性值,返回更新成功
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
複製代碼
什麼狀況下 oldValue 會是個Ref
數據呢?其實看get
部分的時候,咱們就知道啦,reactive
有解套嵌套 ref 數據的能力,如:
const a = {
b: ref(1)
}
const observed = reactive(a) // { b: 1 }
複製代碼
此時,observed.b
輸出的是 1,當作賦值操做 observed.b = 2
時。oldValue
因爲是a.b
,是一個Ref
類型數據,而新的值並非,進而直接修改a.b
的 value 便可。那爲何直接返回,不須要往下觸發 trigger 了呢?是由於在ref
函數中,已經有劫持 set 的邏輯了(不貼代碼了)。
問題 2:何時會target !== toRaw(receiver)
?
在以前的認知中,receiver
有點兒像是this
同樣的存在,指代着被 Proxy 執行後的代理對象。那代理對象用toRaw
轉化,也就是轉爲原始對象,天然跟target
是全等的。這裏就涉及了一個偏門的知識點,詳細介紹能夠看MDN。其中有說到:
Receiver:最初被調用的對象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型鏈上或以其餘方式被間接地調用(所以不必定是 proxy 自己)
這就是代碼中的註釋背後的意義:
don't trigger if target is something up in the prototype chain of original.
舉個實例來講就像這樣:
const child = new Proxy(
{},
{
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('child', receiver)
return true
}
}
)
const parent = new Proxy(
{ a: 10 },
{
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('parent', receiver)
return true
}
}
)
Object.setPrototypeOf(child, parent)
child.a = 4
// 打印結果
// parent Proxy {child: true, a: 4}
// Proxy {child: true, a: 4}
複製代碼
在這種狀況下,這個父對象parent
的set
居然也會被觸發一次,只不過傳遞的receiver
都是child
,進而被更改數據的也一直是child
。在這種狀況下,parent
其實並無變動,按道理來講,它確實不該該觸發它的監聽函數。
問題 3: 數組可能經過方法更新數據,這過程的監聽邏輯是怎麼樣的?
對於一個對象來講,咱們能夠直接賦值屬性值,但對於數組呢?假使const arr = []
,那它既能夠arr[0] = 'value'
,也能夠arr.push('value')
,但並無一個trap
是劫持 push 的。可是當你真正去調試時,發現push
還會觸發兩次set
。
const proxy = new Proxy([], {
set(target, key, value, receiver) {
console.log(key, value, target[key])
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(1)
// 0 1 undefined
// length 1 1
複製代碼
其實push
的內部邏輯就是先給下標賦值,而後設置length
,觸發了兩次set
。不過還有個現象是,雖然push
帶來的length
操做會觸發兩次set
,但走到 length 邏輯時,獲取老的 length 也已是新的值了,因此因爲value === oldValue
,實際只會走到一次trigger
。可是!若是是shift
或unshift
,這樣的邏輯又不成立了,並且若是數組長度是 N,shift
|unshift
就會帶來 N 次的trigger
。這裏其實涉及了Array
的底層實現與規範,我也沒法簡單的闡述明白,建議能夠本身去看ECMA-262中關於Array
的相關標準。
不過這裏確實留下一個小坑,shift
|unshift
以及splice
,會帶來屢次的 effect 觸發。在reacivity
系統中,目前還沒看到相關的優化。固然,真實在使用 vue@3 的過程當中,runtime-core
仍是會針對渲染作批量更新的。
那到這,set
自己的邏輯咱們也摸透了,除了一個外部引入的trigger
。不過咱們知道它是當數據變動時觸發監聽函數的就好,後面再看。
接下來就比較簡單了。
// 劫持屬性刪除
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue })
} else {
trigger(target, OperationTypes.DELETE, key)
}
}
return result
}
// 劫持 in 操做符
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, OperationTypes.HAS, key)
return result
}
// 劫持 Object.keys
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE)
return Reflect.ownKeys(target)
}
複製代碼
這幾個trap
基本沒啥難點了,一眼能看明白。
最後看下readonly
的特殊邏輯:
export const readonlyHandlers: ProxyHandler<any> = {
// 建立get的trap
get: createGetter(true),
// set的trap
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
// 開發環境操做只讀數據報警告。
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
// 若是不可變開關已關閉,則容許設置數據變動
return set(target, key, value, receiver)
}
},
// delete的trap,邏輯跟set差很少
deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String( key )}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},
has,
ownKeys
}
複製代碼
readonly
也很簡單啦,createGetter
的邏輯以前已經看過了。不過有些沒繞過來的同窗可能會想,get
的 trap 又不改變數據,爲何要跟reactive
的作區分,傳個isReadonly
呢?那是由於上文中講到的,經過get
作依賴收集時,對於嵌套的對象數據,是延遲劫持的,因此只能透傳了isReadonly
,讓後續劫持的子對象知道自身是否應該只讀。
has
跟ownKeys
因爲不改變數據,也不用遞歸收集依賴,天然就不用跟可變數據的邏輯作區分了。
看完之後,依賴收集跟觸發監聽函數的時機,咱們就能基本瞭解了。
關於 baseHandles 咱們作個小總結:
Refelct
反射到原始對象上。總結下來,其實仍是比較簡單的。可是咱們還落了對於集合數據的 handlers 沒看,這塊纔是真正的硬骨頭。
打開這個文件,發現這個文件比reactive
跟baseHandlers
可長了很多。沒想到對於這種日常不怎麼用的數據類型的處理,纔是最麻煩的。
看源碼前,其實會有個疑問,爲何Set
|Map
|WeakMap
|WeakSet
這幾個數據須要特殊處理呢?跟其餘數據有什麼區別嗎?咱們點開文件,看看這個handlers
,發現居然是這樣:
export const mutableCollectionHandlers: ProxyHandler<any> = {
get: createInstrumentationGetter(mutableInstrumentations)
}
export const readonlyCollectionHandlers: ProxyHandler<any> = {
get: createInstrumentationGetter(readonlyInstrumentations)
}
複製代碼
只有get
,沒有set
、has
這些。這就懵了,說好的劫持set
跟get
呢?爲何不劫持set
了?緣由是無法這麼作,咱們能夠簡單的作個嘗試:
const set = new Set([1, 2, 3])
const proxy = new Proxy(set, {
get(target, key, receiver) {
console.log(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(target, key, value, receiver)
return Reflect.set(target, key, value, receiver)
}
})
proxy.add(4)
複製代碼
這段代碼,一跑就會出一個錯誤:
Uncaught TypeError: Method Set.prototype.add called on incompatible receiver [object Object]
發現只要劫持set
,或者直接引入了Reflect
,反射行爲到target
上,就會報錯。爲何會這樣呢?這其實也是跟Map|Set
這些的內部實現有關,他們內部存儲的數據必須經過this
來訪問,被成爲所謂的「internal slots」,而經過代理對象去操做時,this
實際上是 proxy,並非 set,因而沒法訪問其內部數據,而數組呢,因爲一些歷史緣由,又是能夠的。詳細解釋能夠見這篇關於 Proxy 的限制的介紹。這篇文章中也提到了解決辦法:
let map = new Map()
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments)
return typeof value == 'function' ? value.bind(target) : value
}
})
proxy.set('test', 1)
複製代碼
大體原理就是,當獲取的是一個函數的時候,將this
綁定爲原始對象,也便是想要劫持的map|set
。這樣就避免了this
的指向問題。
那咱們就有點兒明白,爲何collection
數據須要特殊處理,只劫持一個get
了。具體怎麼作呢?咱們來看代碼。
按慣例先看引用與工具方法:
import { toRaw, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { LOCKED } from './lock'
import {
isObject,
capitalize, // 首字母轉成大寫
hasOwn
} from '@vue/shared'
// 將數據轉爲reactive數據,若是不是對象,則直接返回自身
const toReactive = (value: any) => (isObject(value) ? reactive(value) : value)
const toReadonly = (value: any) => (isObject(value) ? readonly(value) : value)
複製代碼
這些引用,咱們應該基本不用看註釋都能明白了,除了一個工具方法capitalize
須要點開看看外,基本一眼明白。而後咱們須要調整下閱讀順序,先大體看看到底如何經過一個get
的trap
,劫持寫操做。
// proxy handlers
export const mutableCollectionHandlers: ProxyHandler<any> = {
// 建立一個插樁getter
get: createInstrumentationGetter(mutableInstrumentations)
}
複製代碼
首先,咱們要讀懂它的函數名,createInstrumentationGetter
。唔,像我同樣英文比較差的同窗多是不太懂Instrumentation
是什麼意思的。這裏是表達「插樁」的意思。關於「插樁」我很少介紹啦,常見的單測覆蓋率每每就是經過插樁實現的。
在本代碼中,插樁即指向某個方法被注入一段有其餘做用的代碼,目的就是爲了劫持這些方法,增長相應邏輯,那咱們看看此處是如何「插樁」(劫持)的。
// 可變數據插樁對象,以及一系列相應的插樁方法
const mutableInstrumentations: any = {
get(key: any) {
return get(this, key, toReactive)
},
get size() {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false)
}
// 迭代器相關的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false)
readonlyInstrumentations[method] = createIterableMethod(method, true)
})
// 建立getter的函數
function createInstrumentationGetter(instrumentations: any) {
// 返回一個被插樁後的get
return function getInstrumented( target: any, key: string | symbol, receiver: any ) {
// 若是有插樁對象中有此key,且目標對象也有此key,
// 那就用這個插樁對象作反射get的對象,不然用原始對象
target =
hasOwn(instrumentations, key) && key in target ? instrumentations : target
return Reflect.get(target, key, receiver)
}
}
複製代碼
從上文中知道,因爲Proxy
跟collection
數據的原生特性,沒法劫持set
或者直接反射。因此在這裏,建立了一個新的對象,它具備set
跟map
同樣的方法名。這些方法名對應的方法就是插樁後,注入了依賴收集跟響應觸發的方法。而後經過Reflect
反射到這個插樁對象上,獲取的是插樁後的數據,調用的是插樁後的方法。
而對於一些自定義的屬性或方法,Reflect
反射的就不是插樁事後的,而是原數據,對於這些狀況,也不會作響應式的邏輯,好比單測中的:
it('should not observe custom property mutations', () => {
let dummy
const map: any = reactive(new Map())
effect(() => (dummy = map.customProp))
expect(dummy).toBe(undefined)
map.customProp = 'Hello World'
expect(dummy).toBe(undefined)
})
複製代碼
接下來看這個插裝器mutableInstrumentations
,從上往下,咱們先看get
。
const mutableInstrumentations: any = {
get(key: any) {
// this 上述Reflect.get(target, key, receiver)中的target,也便是原始數據
// toReactive是一個將數據轉爲響應式數據的方法
return get(this, key, toReactive)
}
// ...省略其餘
}
function get(target: any, key: any, wrap: (t: any) => any): any {
// 獲取原始數據
target = toRaw(target)
// 因爲Map能夠用對象作key,因此key也有多是個響應式數據,先轉爲原始數據
key = toRaw(key)
// 獲取原始數據的原型對象
const proto: any = Reflect.getPrototypeOf(target)
// 收集依賴
track(target, OperationTypes.GET, key)
// 使用原型方法,經過原始數據去得到該key的值。
const res = proto.get.call(target, key)
// wrap 即傳入的toReceive方法,將獲取的value值轉爲響應式數據
return wrap(res)
}
複製代碼
注意:在get
方法中,第一個入參target
不能跟Proxy
構造函數的第一個入參混淆。Proxy
函數的第一個入參target
指的原始數據。而在get
方法中,這個target
實際上是被代理後的數據。也便是Reflect.get(target, key, receiver)
中的receiver
。
而後咱們就比較清晰了,本質就是經過原始數據的原型方法+call this
,避免了上述的問題,返回真正的數據。
const mutableInstrumentations: any = {
// ...
get size() {
return size(this)
},
has
// ...
}
function size(target: any) {
// 獲取原始數據
target = toRaw(target)
const proto = Reflect.getPrototypeOf(target)
track(target, OperationTypes.ITERATE)
return Reflect.get(proto, 'size', target)
}
function has(this: any, key: any): boolean {
// 獲取原始數據
const target = toRaw(this)
key = toRaw(key)
const proto: any = Reflect.getPrototypeOf(target)
track(target, OperationTypes.HAS, key)
return proto.has.call(target, key)
}
複製代碼
size
跟has
,都是「查」的邏輯。只是size
是一個屬性,不是方法,因此須要以get size()
的方式去劫持。而has
是個方法,不須要專門綁定 this,二者內部邏輯也簡單,跟get
基本一致。不過這裏有個關於 TypeScript 的小細節。has
函數第一個入參是this
,這個在 ts 裏是假的參數,真正調用這個函數的時候,是不須要傳遞的,因此依舊是這樣使用someMap.has(key)
就好。
那除了這兩個查方法,還有迭代器相關的「查」方法。
關於迭代器,若是沒什麼瞭解,建議先閱讀相關文檔,好比MDN。
// 迭代器相關的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false)
})
function createIterableMethod(method: string | symbol, isReadonly: boolean) {
return function(this: any, ...args: any[]) {
// 獲取原始數據
const target = toRaw(this)
// 獲取原型
const proto: any = Reflect.getPrototypeOf(target)
// 若是是entries方法,或者是map的迭代方法的話,isPair爲true
// 這種狀況下,迭代器方法的返回的是一個[key, value]的結構
const isPair =
method === 'entries' ||
(method === Symbol.iterator && target instanceof Map)
// 調用原型鏈上的相應迭代器方法
const innerIterator = proto[method].apply(target, args)
// 獲取相應的轉成響應數據的方法
const wrap = isReadonly ? toReadonly : toReactive
// 收集依賴
track(target, OperationTypes.ITERATE)
// return a wrapped iterator which returns observed versions of the
// values emitted from the real iterator
// 給返回的innerIterator插樁,將其value值轉爲響應式數據
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? // 爲done的時候,value是最後一個值的next,是undefined,不必作響應式轉換了
{ value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
複製代碼
這段邏輯其實還好,核心就是劫持迭代器方法,將每次next
返回的 value 用reactive
轉化。惟一會讓人不清楚的,實際上是對於Iterator
以及Map|Set
的不熟悉。若是確實不熟悉,建議仍是先看下它們的相關文檔。
迭代器相關的還有一個forEach
方法。
function createForEach(isReadonly: boolean) {
// 這個this,咱們已經知道了是假參數,也就是forEach的調用者
return function forEach(this: any, callback: Function, thisArg?: any) {
const observed = this
const target = toRaw(observed)
const proto: any = Reflect.getPrototypeOf(target)
const wrap = isReadonly ? toReadonly : toReactive
track(target, OperationTypes.ITERATE)
// important: create sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
// 將傳遞進來的callback方法插樁,讓傳入callback的數據,轉爲響應式數據
function wrappedCallback(value: any, key: any) {
// forEach使用的數據,轉爲響應式數據
return callback.call(observed, wrap(value), wrap(key), observed)
}
return proto.forEach.call(target, wrappedCallback, thisArg)
}
}
複製代碼
forEach
的邏輯並不複雜,跟上面迭代器部分差很少,也是劫持了方法,將本來的傳參數據轉爲響應式數據後返回。
而後看寫操做。
function add(this: any, value: any) {
// 獲取原始數據
value = toRaw(value)
const target = toRaw(this)
// 獲取原型
const proto: any = Reflect.getPrototypeOf(this)
// 經過原型方法,判斷是否有這個key
const hadKey = proto.has.call(target, value)
// 經過原型方法,增長這個key
const result = proto.add.call(target, value)
// 本來沒有key的話,說明真的是新增,則觸發監聽響應邏輯
if (!hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.ADD, value, { value })
} else {
trigger(target, OperationTypes.ADD, value)
}
}
return result
}
複製代碼
咱們發現寫操做倒簡單多了,其實跟baseHandlers
的邏輯是差很少的,只不過對於那些base
數據,能夠經過Reflect
方便的反射行爲,而在此處,須要手動獲取原型鏈並綁定this
而已。查看set
跟deleteEntry
的代碼,邏輯也差很少,就很少闡述了。
關於readonly
相關的也很簡單了,我也不貼了代碼了,純粹增長文章字數。它就是將add|set|delete|clear
這幾個寫方法再包一層,開發環境下拋個 warning。
到這裏,終於看完了collcetionsHandlers
的所有邏輯了。
再總結一下它是如何劫持 collcetion 數據的。
Set|Map
等集合數據的底層設計問題,Proxy
沒法直接劫持set
或直接反射行爲。get
,對於它的原始方法或屬性,Reflect
反射到插樁器上,不然反射原始對象。toRaw
,獲取代理數據的原始數據,再獲取原始數據的原型方法,而後綁定this
爲原始數據,調取相應方法。getter|has
這類查詢方法,插入收集依賴的邏輯,並將返回值轉爲響應式數據(has 返回 boolean 值故不須要轉換)。其實原理仍是好理解的,只是寫起來比較麻煩。
那到此爲止,終於把reactive
的邏輯徹底理完了。閱讀本部分的代碼有點兒不容易,由於涉及的底層知識比較多,否則會到處懵逼,不過這也是一種學習,探索的過程也是挺有意思的。
在這過程當中,咱們發現,數組的劫持目前仍是存在一點點不足的,直接經過反射,會在一些狀況下重複觸發監聽函數。感受經過相似collection
數據的處理方式能夠解決。可是這又增長了程序複雜度,並且也不知道會不會有一些其餘的坑。
另外,咱們發現閱讀reactivity
相關的代碼時,ts 涉及的沒咱們想象中的多,內部不少狀況下是 any 的,但這是要辯證的看的。首先如小右所說,「這些數據是用戶數據,自己就是any的,勉強要聲明,沒有什麼意義」。並且那一路下來都是很是多的泛型加推導,成本很是高。反正我本身嘗試了下,是無能爲力的。另外當前代碼仍是非正式的階段,若是維護起來過於麻煩。那對於我這種 ts 半吊子的人,若是真的想再貢獻一點代碼,也是舉步維艱。
這篇文章有點兒繁雜,若是是慢慢看下來的,很是感謝你的閱讀~~
下篇是最後的effect
相關的源碼解析,終於能解開最開始targetMap
的謎團,看到track
跟trigger
的內部實現,湊上最後一塊拼圖了。