最近一階段在學習Vue3,Vue3中用 reactive
、ref
等方法將資料轉化為響應式資料,在獲取時使用 track
往 effect
中收集依賴,在值改變時,使用 trigger
觸發依賴,執行對應的監聽函式,這次就先來看一下 reactive
的原始碼。
reactive的原始碼在官方原始碼的packages/reactivity/src/reactive.ts
檔案中,原始碼中提供了四個Api來建立reactive類物件:
- reactive:建立可深入響應的可讀寫物件
- readonly:建立可深入響應的只讀物件
- shallowReactive:建立只有第一層響應的淺可讀寫物件(其他層,值改變檢視不更新)
- shallowReadonly:建立只有一層響應的淺只讀物件
它們都是呼叫createReactiveObject
方法來建立響應式物件,區別在於傳入不同的引數,本文只講reactive,其他幾個大同小異:
export function reactive(target: object) {
// 如果是隻讀的話直接返回
if (isReadonly(target)) {
return target
}
return createReactiveObject(
// 目標物件
target,
// 標識是否是隻讀
false,
// 常用型別攔截器
mutableHandlers,
// 集合型別攔截器
mutableCollectionHandlers,
// 儲了每個物件與代理的map關係
reactiveMap
)
}
export const reactiveMap = new WeakMap<Target, any>()
createReactiveObject程式碼如下:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果代理的資料不是物件,則直接返回原物件
if (!isObject(target)) {
return target
}
// 如果傳入的已經是代理了 並且 不是readonly 轉換 reactive的直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 檢視當前代理物件之前是不是建立過當前代理,如果建立過直接返回之前快取的代理物件
// proxyMap 是一個全域性的快取WeakMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 如果當前物件無法建立代理,則直接返回源物件
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 根據targetType 選擇集合攔截器還是基礎攔截器
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 向全域性快取Map裡儲存
proxyMap.set(target, proxy)
return proxy
}
其中有個方法是 getTargetType
,用來獲取傳入target的型別:
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
export const enum ReactiveFlags {
SKIP = '__v_skip', // 標記阻止成為代理物件
IS_REACTIVE = '__v_isReactive', // 標記一個響應式物件
IS_READONLY = '__v_isReadonly', // 標記一個只讀物件
IS_SHALLOW = '__v_isShallow', // 標記只有一層響應的淺可讀寫物件
RAW = '__v_raw' // 標記獲取原始值
}
const enum TargetType {
// 無效的 比如基礎資料型別
INVALID = 0,
// 常見的 比如object Array
COMMON = 1,
// 集合型別比如 map set
COLLECTION = 2
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
當target被標記為 ReactiveFlags.SKIP
或是 不可擴充的,則會返回 TargetType.INVALID
,無法建立代理,因為Vue需要對Target代理附加很多東西,如果是不可擴充的則會附加失敗;或是使用者主動呼叫 markRaw
等方法將資料標記為非響應式資料,那麼也無法建立代理。
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
看完了入口函式,接下來就是建立Proxy物件的過程了,Vue3會根據getTargetType返回的資料型別來選擇是使用collectionHandlers集合攔截器還是baseHandlers常用攔截器,原因下面講到集合攔截器的時候再說。
常用攔截器baseHandlers:
get
攔截器:function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { if (key === ReactiveFlags.IS_REACTIVE) { // 獲取當前是否是reactive return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { // 獲取當前是否是readonly return isReadonly } else if (key === ReactiveFlags.IS_SHALLOW) { // 獲取當前是否是shallow return shallow } else if ( // 如果獲取源物件,在全域性快取WeakMap中獲取是否有被建立過,如果建立過直接返回被代理物件 key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target } // 是否是陣列 const targetIsArray = isArray(target) // arrayInstrumentations相當於一個改造器,裡面定義了陣列需要改造的方法,進行一些依賴收集等操作 // 如果是陣列,並且訪問的方法在改造器中,則使用改造器獲取 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 獲取結果 const res = Reflect.get(target, key, receiver) if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } // 如果不是隻讀則收集依賴,Vue3中用track收集依賴 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // shallow只有表層響應式,不需要下面去深度建立響應了 if (shallow) { return res } // 如果獲取的值是ref型別 if (isRef(res)) { // 如果是陣列 並且 是int型別的 key,則返回,否則返回.value屬性 return targetIsArray && isIntegerKey(key) ? res : res.value } if (isObject(res)) { // *獲取時才建立相對應型別的代理,將訪問值也轉化為reactive,不是一開始就將所有子資料轉換 return isReadonly ? readonly(res) : reactive(res) } return res } }
注意點是當代理型別是
readonly
時,不會收集依賴。Vue3對於深層次的物件是使用時才建立的
,還有如果結果是ref型別,則需要判斷是否要獲取它的.value型別,舉個?:const Name = ref('張三') const Array = ref([1]) const data = reactive({ name: Name, array: Array }) console.log(Name) // RefImpl型別 console.log(data.name) // 張三 console.log(data.array[0]) // 1
Vue3中使用
arrayInstrumentations
對陣列的部分方法做了處理,為什麼要這麼做呢? 對於push
、pop
、shift
、unshift
、splice
這些方法,寫入和刪除時底層會獲取當前陣列的length屬性,如果我們在effect中使用的話,會收集length屬性的依賴,當使用這些api是也會更改length,就會造成死迴圈:let arr = [] let proxy = new Proxy(arr, { get: function(target, key, receiver) { console.log(key) return Reflect.get(target, key, receiver) } }) proxy.push(1) /* 列印 */ // push // length
// 當把這個程式碼註釋掉時 // if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { // return Reflect.get(arrayInstrumentations, key, receiver); // } const arr = reactive([]) watchEffect(() => { arr.push(1) }) watchEffect(() => { arr.push(2) // 上面的effect裡收集了對length的依賴,push又改變了length,所以上面的又會觸發,以此類推,死迴圈 }) // [1,2,1,2 ...] 死迴圈 console.log(arr)
對於
includes
、indexOf
、lastIndexOf
,內部會去獲取每一個的值,上面講到如果獲取出來的結果是Obejct,會自動轉換為reactive物件:let target = {name: '張三'} const arr = reactive([target]) console.log(arr.indexOf(target)) // -1
因為實際上是
reactive(target)
和target
在對比,當然查不到。set
攔截器function createSetter(shallow = false) { return function set(target, key, value, receiver) { // 獲取舊資料 let oldValue = target[key]; if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false; } // 如果當前不是shallow並且不是隻讀的 if (!shallow && !isReadonly(value)) { if (!isShallow(value)) { // 如果新value本身是響應物件,就把他變成普通物件 // 在get中講到過如果取到的值是物件,才轉換為響應式 // vue3在代理的時候,只代理第一層,在使用到的時候才會代理第二層 value = toRaw(value); oldValue = toRaw(oldValue); } // 如果舊的值是ref物件,新值不是,則直接賦值給ref物件的value屬性 if (!isArray(target) && isRef(oldValue) && !isRef(value)) { // 這裡不觸發trigger是因為,ref物件在value被賦值的時候會觸發寫操作,也會觸發依賴更新 oldValue.value = value; return true; } } const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); // 這個判斷主要是為了處理代理物件的原型也是代理物件的情況 if (target === toRaw(receiver)) { if (!hadKey) { // key不存在就 觸發add型別的依賴更新 trigger(target, "add" /* ADD */, key, value); } else if (hasChanged(value, oldValue)) { // key存在就觸發set型別依賴更新 trigger(target, "set" /* SET */, key, value, oldValue); } } return result; }; }
set中還有一個要注意的地方就是
target === toRaw(receiver)
,這主要是為了處理代理物件的原型也是代理物件的情況:const child = reactive({}) let parentName = '' const parent = reactive({ set name(value) { parentName = value }, get name() { return parentName } }) Object.setPrototypeOf(child, parent) child.name = '張三' console.log(toRaw(child)) // {name: 張三} console.log(parentName) // 張三
當這種時候,如果不加上這個判斷,由於子代理沒有name這個屬性,會觸發原型父代理的set,加上這個判斷避免父代理也觸發更新。
集合攔截器collectionHandlers:
集合型別的資料比較特殊,其相關例項方法Proxy沒有提供相關的捕獲器,但是因為方法呼叫屬於屬性獲取操作,所以都可以通過捕獲get操作來實現,所以Vue3也只定義了get攔截:
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
// 注意這裡
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
之前的文章《代理具有內部插槽的內建物件》中說過Proxy代理具有內部插槽的內建物件,訪問Proxy上的屬性會發生錯誤。Vue3中是如何解決的呢?
Vue3中新建立了一個和集合物件具有相同屬性和方法的普通物件,在集合物件 get 操作時將 target 物件換成新建立的普通物件。這樣,當呼叫 get 操作時 Reflect
反射到這個新物件上,當呼叫 set 方法時就直接呼叫新物件上可以觸發響應的方法,這樣訪問的就不是Proxy上的方法,是這個新物件上的方法:
function createInstrumentations() {
const mutableInstrumentations: Record<string, Function> = {
get(key: unknown) {
return get(this, key)
},
get size() {
return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
}
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
})
return [
mutableInstrumentations
]
}
接下來看一看幾個具體的攔截器:
get
攔截器:function get( target: MapTypes, key: unknown, isReadonly = false, isShallow = false ) { // 如果出現readonly(reactive())這種巢狀的情況,在readonly代理中獲取到reactive() // 確保get時也要經過reactive代理 target = (target as any)[ReactiveFlags.RAW] const rawTarget = toRaw(target) const rawKey = toRaw(key) // 確保 包裝後的key 和 沒包裝的key 都能訪問得到 if (!isReadonly) { if (key !== rawKey) { track(rawTarget, TrackOpTypes.GET, key) } track(rawTarget, TrackOpTypes.GET, rawKey) } const { has } = getProto(rawTarget) const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive if (has.call(rawTarget, key)) { return wrap(target.get(key)) } else if (has.call(rawTarget, rawKey)) { return wrap(target.get(rawKey)) } else if (target !== rawTarget) { target.get(key) } }
集合攔截器裡把
key
和rawKey
都做了處理,保證都能取到資料:let child = { name: 'child' } const childProxy = reactive(child) const map = reactive(new Map()) map.set(childProxy, 1234) console.log(map.get(child)) // 1234 console.log(map.get(childProxy)) // 1234
set
攔截器:// Map set攔截器 function set(this: MapTypes, key: unknown, value: unknown) { // 存origin value value = toRaw(value); // 獲取origin target const target = toRaw(this); const { has, get } = getProto(target); // 檢視當前key是否存在 let hadKey = has.call(target, key); // 如果不存在則獲取 origin if (!hadKey) { key = toRaw(key); hadKey = has.call(target, key); } else if (__DEV__) { // 檢查當前是否包含原始版本 和響應版本在target中,有的話發出警告 checkIdentityKeys(target, has, key); } // 獲取舊的value const oldValue = get.call(target, key); // 設定新值 target.set(key, value); if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); } return this; }
has
攔截器:function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean { // 獲取代理前資料 const target = (this as any)[ReactiveFlags.RAW] const rawTarget = toRaw(target) const rawKey = toRaw(key) // 如果key是響應式的都收集一遍 if (key !== rawKey) { !isReadonly && track(rawTarget, TrackOpTypes.HAS, key) } !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey) // 如果key是Proxy 那麼先訪問 proxyKey 在訪問 原始key 獲取結果 return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey) }
forEach
攔截器:function createForEach(isReadonly: boolean, isShallow: boolean) { return function forEach( this: IterableCollections, callback: Function, thisArg?: unknown ) { const observed = this as any const target = observed[ReactiveFlags.RAW] const rawTarget = toRaw(target) const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY) // 劫持傳遞進來的callback,讓傳入callback的資料轉換成響應式資料 return target.forEach((value: unknown, key: unknown) => { // 確保拿到的值是響應式的 return callback.call(thisArg, wrap(value), wrap(key), observed) }) } }
結尾
我是周小羊,一個前端萌新,寫文章是為了記錄自己日常工作遇到的問題和學習的內容,提升自己,如果您覺得本文對你有用的話,麻煩點個贊鼓勵一下喲~