詳解Vue3中響應式的特殊處理
vue2 vs vue3
兩個響應式更新的核心區(qū)別在于Object.defineProperty 和 Proxy 兩個api 問題,經過這兩個 api 能解決主要的響應式問題。對于一些情況需要特殊處理
vue2 中不能實現(xiàn)的響應式
- arr.length
- arr[0] = newVal
- obj[newKey] = value
- delete obj.key
對于這些情況 vue2 中通過增加Vue.$set和重寫數組方法來實現(xiàn)。然而對于 vue3 中,因為 proxy 是代理整個對象,所以它天生支持一個Object.defineProperty 不能支持的特性,比如他能偵聽到添加新屬性,而 Object.defineProperty因為代理的是每一個 key 所以它對于新增的屬性并不能知道。諸如此類,下面列出一些vue3 中不同的響應式處理。
新增屬性的更新
proxy 雖然能夠監(jiān)聽到新屬性的添加,但新增的屬性并沒有經過像已有屬性那樣的 getter 收集依賴,也就是并不能觸發(fā)更新。所以我們的目的變成如何收集響應
首先,我們先看下 vue3 中是如何處理 for...in.. 循環(huán)的,可以知道的是循環(huán)的內部使用了Reflect.ownKeys(obj) 來獲取只屬于對象自身擁有的鍵。所以對于 for..in 循環(huán)的攔截就可以清楚了
const obj = {foo: 1}
const ITERATE_KEY = symbol()
const p = new Proxy(obj, {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
})這里,我們用了一個symbol 的數據作為 收集依賴的key 因為這個是我們在遍歷中的攔截操作沒有與具體的 key 關聯(lián),而是一個整體性的攔截。在觸發(fā)響應時,只要觸發(fā)這個 symbol 收集的 effect 就可以
trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // 數組的情況追蹤 length
這里會發(fā)生影響遍歷對象長度時,會引ITERATE_KEY 相關的副作用函數執(zhí)行
effect(() => {
for(let i in obj) {
console.log(i)
}
})副作用函數執(zhí)行后,類比我們執(zhí)行了渲染函數。 然后回到我們的新增屬性,
p.newKey = 1
因為新增屬性,會對for.. in .. 循環(huán)產生影響,所以我們需要把與 ITERATE_KEY 相關的副作用函數拿出來重新執(zhí)行,看看源碼中這塊的處理
首先這里是 setter 的處理
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
...
// 這里是表明是否有 key 也就是判斷是否是新增元素
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: 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)) {
if (!hadKey) {
// 這里表明是新增元素時,走的 trigger 邏輯
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
然后是 具體的 trigger,拿到對應的標識去更新effect
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
...
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
// 這種情況就是我們剛剛確定的 trigger 的執(zhí)行
case TriggerOpTypes.ADD:
if (!isArray(target)) {
// 拿到收集的 ITERATE_KEY 的依賴
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
...
}
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
...
triggerEffects(createDep(effects))
...
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
// 執(zhí)行所有的 effect 函數
effect.run()
}
}
}
總結:
在遍歷對象或數組時,用一個唯一標識symbol 收集依賴
- 其實如果在模板中直接使用 obj 會伴隨著一個 JSON.stringify 的過程,也會伴隨著收集依賴。
- 我們在 js 代碼里如果沒有用到遍歷對象,單獨對一個對象新增是不會觸發(fā)更新的,因為沒有收集的過程。
在設置新值時,獲取收集的symbol對應的副作用函數更新
遍歷數組方法的處理
在使用數組時,會伴隨著 this 的問題導致代理對象拿不到屬性的問題,比如
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj) // false
之所以會出現(xiàn)這樣的問題,是因為 includes 內部的this 指向的是代理對象 arr, 并且在因為比較去獲取元素時拿到的也是代理對象,所以拿原始對象去找肯定找不到。所以,我們需要去修改inlcudes的行為,
new Proxy(obj, {
get() {
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
}
})
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
// 處理集中數組遍歷方法中的問題。
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
// 先使用原始參數,可能是原始對象,也可能是代理對象。
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
// 如果沒有找到,就拿到原始參數去比較,脫完響應式的數據。
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})數組的變更方法
對于可能會更改原數組長度的數組方法,push, pop, shift, unshift, splice 也需要進行處理,否則,就會陷入無限遞歸當中,考慮下面的場景
cosnt arr = reactive([])
effect(() => {
arr.push(1)
})
effect(() => {
arr.push(1)
})這兩個的執(zhí)行過程如下:
- 首先,第一個副作用函數執(zhí)行,然后數組中添加1, 并且這個過程會給影響數組的
length, 所以會與length會被track, 建立響應式聯(lián)系。 - 然后第二個副作用函數執(zhí)行,執(zhí)行
push這時因為影響了length,先track建立響應式聯(lián)系,然后會試圖拿出length的副作用函數也就是第一個副作用函數執(zhí)行,然而這時第二個副作用函數還未執(zhí)行完成,就又開始執(zhí)行第一個副作用函數了, - 第一個副作用函數再次執(zhí)行,同樣會讀取
length并且設置length,重復上面收集和更新的過程,又要把第二個副作用中收集的 length 執(zhí)行 - 如此循環(huán)往復。最終會棧溢出。
所以問題的關鍵就在于 length 的不斷讀取和設置。所以我們需要在讀取到 length,避免它與副作用函數之間建立聯(lián)系
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 禁止追蹤變化,
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
// 等函數執(zhí)行完畢時再回復追蹤。
resetTracking()
return res
}
})總結
vue2和 vue3 對于新增的屬性都是需要先獲取之前收集到的依賴,才能夠派發(fā)更新。vue2 中使用 Vue.$set借助的是新增屬性的對象已有的依賴派發(fā)更新。vue3 是由于 ownKeys()攔截之前收集到的 symbol 依賴,在添加屬性時觸發(fā)這個symbol 收集到的依賴更新。
對于數組,vue2 是攔截修改數組的方法,然后把當前數組所收集的依賴做出更新執(zhí)行ob.dep.notify(),vue3因為本身就有一定的能力實現(xiàn)數組元素的更新,只是由于是因為length 導致的棧溢出,所以采用禁止跟蹤解決,同時,訪問方法也需要更新是因為代理對象和原始對象不一致問題,在查找時讓對比兩個解決。
以上就是詳解Vue3中響應式的特殊處理的詳細內容,更多關于Vue3響應式的資料請關注腳本之家其它相關文章!
相關文章
vue如何動態(tài)綁定img的src屬性(v-bind)
這篇文章主要介紹了vue如何動態(tài)綁定img的src屬性(v-bind),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
vue.js 底部導航欄 一級路由顯示 子路由不顯示的解決方法
下面小編就為大家分享一篇vue.js 底部導航欄 一級路由顯示 子路由不顯示的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03
vue-router3.0版本中 router.push 不能刷新頁面的問題
這篇文章主要介紹了vue-router3.0版本中 router.push 不能刷新頁面的問題,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05
詳解vuex之store拆分即多模塊狀態(tài)管理(modules)篇
這篇文章主要介紹了詳解vuex之store拆分即多模塊狀態(tài)管理(modules)篇,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11

