Vue3資料響應系統
vue3 就是基於 Proxy對其資料響應系統進行了重寫,現在這部分可以作為獨立的模組配合其他框架使用。資料響應可分為三個階段:初始化階段 --> 依賴收集階段 --> 資料響應階段
Proxy代理須知
用 Proxy做代理時,我們需要了解幾個問題:
1、 Proxy代理是如何對其 trap進行處理來實現資料響應的?也就是其 get/set裡面是如何做攔截處理(其實這裡的trap預設行為可以通過 Reflect來返回, Reflect物件的方法與 Proxy物件的方法一一對應,只要是 Proxy物件的方法,就能在 Reflect物件上找到對應的方法。這就讓 Proxy物件可以方便地呼叫對應的 Reflect方法,完成預設行為,作為修改行為的基礎。這裡具體可以檢視阮大神的ES6入門)
2、 Proxy代理的物件只能代理到第一層,當代理的物件多層巢狀時,那麼物件內部的深度監測需要如何去實現?
3、當代理物件是陣列時,比如push操作會觸發多次 get/set,因為push操作除了增加陣列的資料項之外,也會引發陣列本身其他相關屬性的改變,因此會多次觸發 get/set,那麼要如何解決呢?
下面我們會稍微分析下vue3 針對這幾個問題做了哪些優化處理。
初始化階段
初始化過程相對比較簡單,通過reactive()方法將資料轉化成 Proxy物件,這裡注意一個比較重要的物件 targetMap,它在依賴收集階段起著比較重要的作用,具體下面會有分析。
export function react ive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
...
// 建立proxy物件
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
}
// 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
}
// 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)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
Vue3 如何進行深度觀測的?先看下面這段程式碼
let data = { x: {y: {z: 1 } } }
let p = new Proxy(data, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log('get value:', key)
console.log(res)
return res
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.x.y = 2
// get value: x
// {y: 2}
上面程式碼我們可以知道 Proxy只會代理一層,因為這裡只是觸發了一次最外層屬性 x的 get,而重新賦值的其內部屬性 y,此時 set並沒有被觸發,所以改變內部屬性是不會監測到的。繼續看,Reflect.get返回的結果正是 target的內層結構,此時 p.x.y的值也已經變成 2了,我們可以判斷當前 Reflect.get返回的值是否為 object,若是則再通過 reactive做代理,這樣就達到了深度觀測的目的了。
Vue3實現過程具體我們可以看下面原始碼:
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
// 當代理的物件是多層結構時,Reflect.get會返回物件的內層結構,我們可以拿到當前res再做判斷是否為object,進而進行reactive,就達到了深度觀測的目的了
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
依賴收集階段
所謂的依賴在Vue3可簡單理解為各種 effect響應式函式,其中包括了屬性依賴的 effect,計算屬性 computedEffect以及元件檢視的 componentEffect
1、在檢視掛載渲染時會執行一個 componentEffect,觸發相關資料屬性getter操作來完成檢視依賴收集。
2、 effect函式執行也會觸發相關屬性的getter操作,此時操作了某個屬性的 effect也會被該屬性對應進行收集(注意這裡的屬性是可觀測的)。
之所以說是響應式的,是因為effect方法回撥中關聯了被觀測的資料屬性,而effect一般是立即執行的,此時觸發了該屬性的 getter,進行依賴收集,當該屬性觸發 setter時,便會觸發執行收集的依賴。另外,這裡每次effect執行時,當前的effect會被壓入一個名為 activeReactiveEffectStack的棧中,是在依賴收集的時候使用。
export function effect(
fn: Function,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
if ((fn as ReactiveEffect).isEffect) {
fn = (fn as ReactiveEffect).raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// effect立即執行,觸發effect回撥函式fn中相關響應資料屬性的getter操作,從而進行依賴收集
effect()
}
return effect
}
...
// 觸發getter操作,進行依賴收集
export function track(
target: any,
type: OperationTypes,
key?: string | symbol
) {
if (!shouldTrack) {
return
}
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (effect) {
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
// 防止依賴重複收集
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
if (__DEV__ && effect.onTrack) {
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
}
開頭說過 targetMap物件在依賴收集過程中的重要作用,看原始碼我們大概知道了,它維護了一個依賴收集的關係表, targetMap是一個 WeakMap,其 key值是當前被代理的物件 target,而 value則是該物件所對應的 depsMap,它是一個 Map, key值為觸發 getter時的屬性值,而 value值則是觸發過該屬性值所對應的各個 effect。
故 targetMap的關係對映可以看成 target --> key --> effect,可以看出 target被觀測後,其屬性 key在被觸發 getter操作時,收集了所依賴的 effect,可以說 targetMap是Vue3進行依賴收集的一個核心物件。
PPT模板下載大全https://www.wode007.com
響應階段
當觸發屬性 setter時,通過 trigger函式會執行屬性對應收集的 effects,也包括 computedEffects,此時通過 scheduleRun逐個呼叫 effect,最後完成檢視更新。
上面我們講過監測陣列的時候可能觸發多次 get/set, 那麼如何防止觸發多次的呢?先看Vue3的原始碼(簡寫省略了部分程式碼):
// setter操作觸發響應
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
value = toRaw(value)
// 判斷key是否為當前target自身屬性
const hadKey = hasOwn(target, key)
// 獲取舊值
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
...
if (!hadKey) {
// 若屬性不存在標記為add操作
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
// 若值不相等在觸發,並且標記為set操作
trigger(target, OperationTypes.SET, key)
}
...
return result
}
export function trigger(
target: any,
type: OperationTypes,
key?: string | symbol,
extraInfo?: any
) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// 這裡遍歷找出相關依賴的effect
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
// 這裡當改變陣列length長度時也會觸發相關effect進行響應
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
// 遍歷執行依賴的effect
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
computedRunners.forEach(run)
effects.forEach(run)
}
function scheduleRun(
effect: ReactiveEffect,
target: any,
type: OperationTypes,
key: string | symbol | undefined,
extraInfo: any
) {
...
if (effect.scheduler !== void 0) {
effect.scheduler(effect)
} else {
effect()
}
}
由原始碼我們可以分析出:1、判斷key是否為當前被代理物件target自身屬性; 2、判斷舊值與新值是否相等。只有這兩個條件其中一個滿足,才有可能執行 trigger。
怎麼理解呢,我們舉個:chestnut:,可以實現一個小的 reactive方法來做資料代理,程式碼如下:
function hasOwn(val, key) {
const hasOwnProperty = Object.prototype.hasOwnProperty
return hasOwnProperty.call(val, key)
}
function reactive(data) {
let observed = new Proxy(data, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
return res
},
set(target, key, value, receiver) {
console.log(target, key, value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (!hadKey) {
console.log('trigger add operation...')
} else if(value !== oldValue) {
console.log('trigger set operation...')
}
return result
}
})
return observed
}
let data = ['a', 'b']
let state = reactive(data)
state.push('c')
// ["a", "b"] "2" "c"
// trigger add operation...
// ["a", "b", "c"] "length" 3
state.push(‘c’)會觸發兩次 set,一次是push的值 c,一次是 length屬性設定。
1、設定值 c時,新增了索引 key為 2,*target 是原始的代理物件 [‘a’, ‘c’],這是一個add操作, 故 hasOwn(target, key)返回的是false,此時執行 trigger add operation…。注意在trigger方法中, length沒有對應的 effect,所以就沒有執行相關的 effect。
2、當傳入 key為 length時, length是自身屬性,故 hasOwn(target, key)返回 true, 此時 value是 3, 而 oldValue即為 target[‘length’]也是 3,故 value !== oldValue不成立,不執行 trigger方法
故只有當 hasOwn(target, key)返回true或者 value !== oldValue的時候才執行 trigger。
總結
在分析原始碼之前我們先列舉了用Proxy做代理實現資料響應需要解決的幾個問題,並帶著這些問題一步一步揭開Vue在資料響應系統處理這些問題的面紗,也讓我們進一步瞭解了Vue原始碼編寫有許多巧妙的地方,比如利用 Reflect.get返回值為 target當前觸發的第一層屬性 key值對應的 value值,從而再來判斷是否為Object來進行深度觀測,並且觀測的值存放在一個WeakMap下,這樣相比較遞迴Proxy,Vue的這種實現方式大大提高了資料響應的效能。