詳解Vue3中響應(yīng)式的特殊處理
vue2 vs vue3
兩個(gè)響應(yīng)式更新的核心區(qū)別在于Object.defineProperty
和 Proxy
兩個(gè)api 問(wèn)題,經(jīng)過(guò)這兩個(gè) api 能解決主要的響應(yīng)式問(wèn)題。對(duì)于一些情況需要特殊處理
vue2 中不能實(shí)現(xiàn)的響應(yīng)式
- arr.length
- arr[0] = newVal
- obj[newKey] = value
- delete obj.key
對(duì)于這些情況 vue2 中通過(guò)增加Vue.$set
和重寫(xiě)數(shù)組方法來(lái)實(shí)現(xiàn)。然而對(duì)于 vue3 中,因?yàn)?proxy
是代理整個(gè)對(duì)象,所以它天生支持一個(gè)Object.defineProperty
不能支持的特性,比如他能偵聽(tīng)到添加新屬性,而 Object.defineProperty
因?yàn)榇淼氖敲恳粋€(gè) key
所以它對(duì)于新增的屬性并不能知道。諸如此類(lèi),下面列出一些vue3 中不同的響應(yīng)式處理。
新增屬性的更新
proxy 雖然能夠監(jiān)聽(tīng)到新屬性的添加,但新增的屬性并沒(méi)有經(jīng)過(guò)像已有屬性那樣的 getter 收集依賴(lài),也就是并不能觸發(fā)更新。所以我們的目的變成如何收集響應(yīng)
首先,我們先看下 vue3 中是如何處理 for...in..
循環(huán)的,可以知道的是循環(huán)的內(nèi)部使用了Reflect.ownKeys(obj
) 來(lái)獲取只屬于對(duì)象自身?yè)碛械逆I。所以對(duì)于 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) })
這里,我們用了一個(gè)symbol
的數(shù)據(jù)作為 收集依賴(lài)的key
因?yàn)檫@個(gè)是我們?cè)诒闅v中的攔截操作沒(méi)有與具體的 key 關(guān)聯(lián),而是一個(gè)整體性的攔截。在觸發(fā)響應(yīng)時(shí),只要觸發(fā)這個(gè) symbol
收集的 effect
就可以
trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // 數(shù)組的情況追蹤 length
這里會(huì)發(fā)生影響遍歷對(duì)象長(zhǎng)度時(shí),會(huì)引ITERATE_KEY
相關(guān)的副作用函數(shù)執(zhí)行
effect(() => { for(let i in obj) { console.log(i) } })
副作用函數(shù)執(zhí)行后,類(lèi)比我們執(zhí)行了渲染函數(shù)。 然后回到我們的新增屬性,
p.newKey = 1
因?yàn)樾略鰧傩?,?huì)對(duì)for.. in ..
循環(huán)產(chǎn)生影響,所以我們需要把與 ITERATE_KEY
相關(guān)的副作用函數(shù)拿出來(lái)重新執(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) { // 這里表明是新增元素時(shí),走的 trigger 邏輯 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }
然后是 具體的 trigger
,拿到對(duì)應(yīng)的標(biāo)識(shí)去更新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 的依賴(lài) 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 函數(shù) effect.run() } } }
總結(jié):
在遍歷對(duì)象或數(shù)組時(shí),用一個(gè)唯一標(biāo)識(shí)symbol
收集依賴(lài)
- 其實(shí)如果在模板中直接使用 obj 會(huì)伴隨著一個(gè) JSON.stringify 的過(guò)程,也會(huì)伴隨著收集依賴(lài)。
- 我們?cè)?js 代碼里如果沒(méi)有用到遍歷對(duì)象,單獨(dú)對(duì)一個(gè)對(duì)象新增是不會(huì)觸發(fā)更新的,因?yàn)闆](méi)有收集的過(guò)程。
在設(shè)置新值時(shí),獲取收集的symbol
對(duì)應(yīng)的副作用函數(shù)更新
遍歷數(shù)組方法的處理
在使用數(shù)組時(shí),會(huì)伴隨著 this
的問(wèn)題導(dǎo)致代理對(duì)象拿不到屬性的問(wèn)題,比如
const obj = {} const arr = reactive([obj]) console.log(arr.includes(obj) // false
之所以會(huì)出現(xiàn)這樣的問(wèn)題,是因?yàn)?includes
內(nèi)部的this
指向的是代理對(duì)象 arr, 并且在因?yàn)楸容^去獲取元素時(shí)拿到的也是代理對(duì)象,所以拿原始對(duì)象去找肯定找不到。所以,我們需要去修改inlcudes的行為,
new Proxy(obj, { get() { if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } } }) const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations() // 處理集中數(shù)組遍歷方法中的問(wèn)題。 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) // 先使用原始參數(shù),可能是原始對(duì)象,也可能是代理對(duì)象。 const res = arr[key](...args) if (res === -1 || res === false) { // if that didn't work, run it again using raw values. // 如果沒(méi)有找到,就拿到原始參數(shù)去比較,脫完響應(yīng)式的數(shù)據(jù)。 return arr[key](...args.map(toRaw)) } else { return res } } })
數(shù)組的變更方法
對(duì)于可能會(huì)更改原數(shù)組長(zhǎng)度的數(shù)組方法,push
, pop
, shift
, unshift
, splice
也需要進(jìn)行處理,否則,就會(huì)陷入無(wú)限遞歸當(dāng)中,考慮下面的場(chǎng)景
cosnt arr = reactive([]) effect(() => { arr.push(1) }) effect(() => { arr.push(1) })
這兩個(gè)的執(zhí)行過(guò)程如下:
- 首先,第一個(gè)副作用函數(shù)執(zhí)行,然后數(shù)組中添加1, 并且這個(gè)過(guò)程會(huì)給影響數(shù)組的
length
, 所以會(huì)與length
會(huì)被track
, 建立響應(yīng)式聯(lián)系。 - 然后第二個(gè)副作用函數(shù)執(zhí)行,執(zhí)行
push
這時(shí)因?yàn)橛绊懥?length
,先track
建立響應(yīng)式聯(lián)系,然后會(huì)試圖拿出length的副作用函數(shù)也就是第一個(gè)副作用函數(shù)執(zhí)行,然而這時(shí)第二個(gè)副作用函數(shù)還未執(zhí)行完成,就又開(kāi)始執(zhí)行第一個(gè)副作用函數(shù)了, - 第一個(gè)副作用函數(shù)再次執(zhí)行,同樣會(huì)讀取
length
并且設(shè)置length
,重復(fù)上面收集和更新的過(guò)程,又要把第二個(gè)副作用中收集的 length 執(zhí)行 - 如此循環(huán)往復(fù)。最終會(huì)棧溢出。
所以問(wèn)題的關(guān)鍵就在于 length
的不斷讀取和設(shè)置。所以我們需要在讀取到 length,避免它與副作用函數(shù)之間建立聯(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) // 等函數(shù)執(zhí)行完畢時(shí)再回復(fù)追蹤。 resetTracking() return res } })
總結(jié)
vue2
和 vue3
對(duì)于新增的屬性都是需要先獲取之前收集到的依賴(lài),才能夠派發(fā)更新。vue2
中使用 Vue.$set
借助的是新增屬性的對(duì)象已有的依賴(lài)派發(fā)更新。vue3
是由于 ownKeys()
攔截之前收集到的 symbol
依賴(lài),在添加屬性時(shí)觸發(fā)這個(gè)symbol
收集到的依賴(lài)更新。
對(duì)于數(shù)組,vue2
是攔截修改數(shù)組的方法,然后把當(dāng)前數(shù)組所收集的依賴(lài)做出更新執(zhí)行ob.dep.notify()
,vue3
因?yàn)楸旧砭陀幸欢ǖ哪芰?shí)現(xiàn)數(shù)組元素的更新,只是由于是因?yàn)?code>length 導(dǎo)致的棧溢出,所以采用禁止跟蹤解決,同時(shí),訪問(wèn)方法也需要更新是因?yàn)榇韺?duì)象和原始對(duì)象不一致問(wèn)題,在查找時(shí)讓對(duì)比兩個(gè)解決。
以上就是詳解Vue3中響應(yīng)式的特殊處理的詳細(xì)內(nèi)容,更多關(guān)于Vue3響應(yīng)式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue關(guān)鍵字搜索功能實(shí)戰(zhàn)小案例
在vue項(xiàng)目中,搜索功能是我們經(jīng)常需要使用的一個(gè)場(chǎng)景,下面這篇文章主要給大家介紹了關(guān)于Vue關(guān)鍵字搜索功能的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06vue如何動(dòng)態(tài)綁定img的src屬性(v-bind)
這篇文章主要介紹了vue如何動(dòng)態(tài)綁定img的src屬性(v-bind),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04vue中使用require動(dòng)態(tài)獲取圖片地址方式
這篇文章主要介紹了vue中使用require動(dòng)態(tài)獲取圖片地址方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06vue.js實(shí)現(xiàn)h5機(jī)器人聊天(測(cè)試版)
這篇文章主要為大家詳細(xì)介紹了vue.js實(shí)現(xiàn)h5機(jī)器人聊天測(cè)試版,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07vue實(shí)現(xiàn)動(dòng)態(tài)控制表格列的顯示隱藏
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)動(dòng)態(tài)控制表格列的顯示隱藏,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04vue.js 底部導(dǎo)航欄 一級(jí)路由顯示 子路由不顯示的解決方法
下面小編就為大家分享一篇vue.js 底部導(dǎo)航欄 一級(jí)路由顯示 子路由不顯示的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue-router3.0版本中 router.push 不能刷新頁(yè)面的問(wèn)題
這篇文章主要介紹了vue-router3.0版本中 router.push 不能刷新頁(yè)面的問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05詳解vuex之store拆分即多模塊狀態(tài)管理(modules)篇
這篇文章主要介紹了詳解vuex之store拆分即多模塊狀態(tài)管理(modules)篇,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11