您好,登錄后才能下訂單哦!
本篇內容介紹了“Vue3中響應式的特殊處理方法是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
兩個響應式更新的核心區別在于Object.defineProperty
和 Proxy
兩個api 問題,經過這兩個 api 能解決主要的響應式問題。對于一些情況需要特殊處理
vue2 中不能實現的響應式
arr.length
arr[0] = newVal
obj[newKey] = value
delete obj.key
對于這些情況 vue2 中通過增加Vue.$set
和重寫數組方法來實現。然而對于 vue3 中,因為 proxy
是代理整個對象,所以它天生支持一個Object.defineProperty
不能支持的特性,比如他能偵聽到添加新屬性,而 Object.defineProperty
因為代理的是每一個 key
所以它對于新增的屬性并不能知道。諸如此類,下面列出一些vue3 中不同的響應式處理。
proxy 雖然能夠監聽到新屬性的添加,但新增的屬性并沒有經過像已有屬性那樣的 getter 收集依賴,也就是并不能觸發更新。所以我們的目的變成如何收集響應
首先,我們先看下 vue3 中是如何處理 for...in..
循環的,可以知道的是循環的內部使用了Reflect.ownKeys(obj
) 來獲取只屬于對象自身擁有的鍵。所以對于 for..in
循環的攔截就可以清楚了
const obj = {foo: 1} const ITERATE_KEY = symbol() const p = new Proxy(obj, { track(target, ITERATE_KEY) return Reflect.ownKeys(target) })
這里,我們用了一個symbol
的數據作為 收集依賴的key
因為這個是我們在遍歷中的攔截操作沒有與具體的 key 關聯,而是一個整體性的攔截。在觸發響應時,只要觸發這個 symbol
收集的 effect
就可以
trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // 數組的情況追蹤 length
這里會發生影響遍歷對象長度時,會引ITERATE_KEY
相關的副作用函數執行
effect(() => { for(let i in obj) { console.log(i) } })
副作用函數執行后,類比我們執行了渲染函數。 然后回到我們的新增屬性,
p.newKey = 1
因為新增屬性,會對for.. in ..
循環產生影響,所以我們需要把與 ITERATE_KEY
相關的副作用函數拿出來重新執行,看看源碼中這塊的處理
首先這里是 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 的執行 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 { // 執行所有的 effect 函數 effect.run() } } }
總結:
在遍歷對象或數組時,用一個唯一標識symbol
收集依賴
其實如果在模板中直接使用 obj 會伴隨著一個 JSON.stringify 的過程,也會伴隨著收集依賴。
我們在 js 代碼里如果沒有用到遍歷對象,單獨對一個對象新增是不會觸發更新的,因為沒有收集的過程。
在設置新值時,獲取收集的symbol
對應的副作用函數更新
在使用數組時,會伴隨著 this
的問題導致代理對象拿不到屬性的問題,比如
const obj = {} const arr = reactive([obj]) console.log(arr.includes(obj) // false
之所以會出現這樣的問題,是因為 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) })
這兩個的執行過程如下:
首先,第一個副作用函數執行,然后數組中添加1, 并且這個過程會給影響數組的 length
, 所以會與 length
會被 track
, 建立響應式聯系。
然后第二個副作用函數執行,執行 push
這時因為影響了 length
,先track
建立響應式聯系,然后會試圖拿出length的副作用函數也就是第一個副作用函數執行,然而這時第二個副作用函數還未執行完成,就又開始執行第一個副作用函數了,
第一個副作用函數再次執行,同樣會讀取length
并且設置 length
,重復上面收集和更新的過程,又要把第二個副作用中收集的 length 執行
如此循環往復。最終會棧溢出。
所以問題的關鍵就在于 length
的不斷讀取和設置。所以我們需要在讀取到 length,避免它與副作用函數之間建立聯系
;(['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) // 等函數執行完畢時再回復追蹤。 resetTracking() return res } })
“Vue3中響應式的特殊處理方法是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。