欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3響應(yīng)式對象是如何實(shí)現(xiàn)的(2)

 更新時間:2022年08月30日 11:29:07   作者:???????咕咕雞_  
這篇文章主要介紹了Vue3響應(yīng)式對象是如何實(shí)現(xiàn)的,文章基于上篇文章展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下

前言

Vue3響應(yīng)式對象是如何實(shí)現(xiàn)的(1)中,我們已經(jīng)從功能上實(shí)現(xiàn)了一個響應(yīng)式對象。如果僅僅滿足于功能實(shí)現(xiàn),我們就可以止步于此了。但在上篇中,我們僅考慮了最簡單的情況,想要完成一個完整可用的響應(yīng)式,需要我們繼續(xù)對細(xì)節(jié)深入思考。在特定場景下,是否存在BUG?是否還能繼續(xù)優(yōu)化?

分支切換的優(yōu)化

在上篇中,收集副作用函數(shù)是利用get自動收集。那么被get自動收集的副作用函數(shù),是否有可能會產(chǎn)生多余的觸發(fā)呢?或者說,我們其實(shí)進(jìn)行了多余的收集呢?同樣,還是從一個例子入手。

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}

const objsMap = new WeakMap()
const data = { text: 'hello vue', ok: true } // (1)
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    trigger(target, key)
    return true
  }
})
function track(target, key) {
  if(!activeEffect) return
  let propsMap = objsMap.get(target)
  if(!propsMap) {
    objsMap.set(target, (propsMap = new Map()))
  }
  let fns = propsMap.get(key)
  if(!fns) {
    propsMap.set(key, (fns = new Set()))
  }
  fns.add(activeEffect)
}

function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  fns && fns.forEach(fn => fn())
}

function fn() {
  document.body.innerText = obj.ok ? obj.text : 'ops...' // (2)
  console.log('Done!')
}
effect(fn)

這段代碼中,我們做了(1)(2)兩處更改。我們在(1)處給響應(yīng)式對象新增加了一個boolean類型的屬性ok,在(2)處我們利用ok的真值,來選擇將誰賦值給document.body.innerText。現(xiàn)在,我們將obj.ok的值置為false,這就意味著,document.body.innerText的值不再依賴于obj.text,而直接取字符串'ops...'。

此時,我們要能夠注意到一件事,雖然document.body.innerText的值不再依賴于obj.text了,但由于ok的初值是true,也就意味著在ok的值沒有改變時,document.body.innerText的值依賴于obj.text,更進(jìn)一步說,這個函數(shù)已經(jīng)被obj.text當(dāng)作自己的副作用函數(shù)收集了。這會導(dǎo)致什么呢?

我們更改了obj.text的值,這會觸發(fā)副作用函數(shù)。但此時由于ok的值為false,界面上顯示的內(nèi)容沒有發(fā)生任何改變。也就是說,此時修改obj.text觸發(fā)的副作用函數(shù)的更新是不必要的。

這部分有些繞,讓我們通過畫圖來嘗試說明。當(dāng)oktrue時,數(shù)據(jù)結(jié)構(gòu)的狀態(tài)如圖所示:

從圖中可以看到,obj.textobj.ok都收集了同一個副作用函數(shù)fn。這也解釋了為什么即使我們將obj.ok的值為false,更改obj.text仍然會觸發(fā)副作用函數(shù)fn。

我們希望的理想狀況是,當(dāng)okfalse時,副作用函數(shù)fn被從obj.text的副作用函數(shù)收集器中刪除,數(shù)據(jù)結(jié)構(gòu)的狀態(tài)能改變?yōu)槿缦聽顟B(tài)。

這就要求我們能夠在每次執(zhí)行副作用函數(shù)前,將該副作用函數(shù)從相關(guān)的副作用函數(shù)收集器中刪除,再重新建立聯(lián)系。為了實(shí)現(xiàn)這一點(diǎn),就要求我們記錄哪些副作用函數(shù)收集器收集了該副作用函數(shù)。

let activeEffect
function cleanup(effectFn) { // (3)
  for(let i = 0; i < effectFn.deps.length; i++) {
    const fns = effectFn.deps[i]
    fns.delete(effectFn)
  }
  effectFn.deps.length = 0
}
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }
  effectFn.deps = [] // (1)
  effectFn()
}

const objsMap = new WeakMap()
const data = { text: 'hello vue', ok: true }
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    trigger(target, key)
    return true
  }
})

function track(target, key) {
  if(!activeEffect) return
  let propsMap = objsMap.get(target)
  if(!propsMap) {
    objsMap.set(target, (propsMap = new Map()))
  }
  let fns = propsMap.get(key)
  if(!fns) {
    propsMap.set(key, (fns = new Set()))
  }
  fns.add(activeEffect)
  activeEffect.deps.push(fns) // (2)
}

function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  fns && fns.forEach(fn => fn())
}

function fn() {
  document.body.innerText = obj.ok ? obj.text : 'ops...'
  console.log('Done!')
}
effect(fn)

在這段代碼中,我們增加了3處改動。為了記錄副作用函數(shù)被哪些副作用函數(shù)收集器收集,我們在(1)處給每個副作用函數(shù)掛載了一個deps,用于記錄該副作用函數(shù)被誰收集。在(2)處,副作用函數(shù)被收集時,我們記錄副作用函數(shù)收集器。在(3)處,我們新增了cleanup函數(shù),從含有該副作用函數(shù)的副作用函數(shù)收集器中,刪除該副作用函數(shù)。

看上去好像沒啥問題了,但是運(yùn)行代碼會發(fā)現(xiàn)產(chǎn)生了死循環(huán)。問題出在哪呢?

以下面這段代碼為例:

const set = new Set([1])
set.forEach(item => {
    set.delete(1)
    set.add(1)
    console.log('Done!')
})

是的,這段代碼會產(chǎn)生死循環(huán)。原因是ECMAScript對Set.prototype.forEach的規(guī)范中明確,使用forEach遍歷Set時,如果有值被直接添加到該Set上,則forEach會再次訪問該值。

  const effectFn = () => {
    cleanup(effectFn) // (1)
    activeEffect = effectFn
    fn() // (2)
  }

同理,我們的代碼中,當(dāng)effectFn被執(zhí)行時,(1)處的cleanup清除副作用函數(shù),就相當(dāng)于set.delete;而(2)處執(zhí)行副作用函數(shù)fn時,會觸發(fā)依賴收集,將副作用函數(shù)又加入到了副作用函數(shù)收集器中,相當(dāng)于set.add,從而造成死循環(huán)。

解決的方法也很簡單,我們只需要避免在原Set上直接進(jìn)行遍歷即可。

const set = new Set([1])
const otherSet = new Set(set)
otherSet.forEach(item => {
    set.delete(1)
    set.add(1)
    console.log('Done!')
})

在上例中,我們復(fù)制了setotherset中,otherset僅會執(zhí)行set.length次。按照這個思路,修改我們的代碼。

let activeEffect

function cleanup(effectFn) { 
  for(let i = 0; i < effectFn.deps.length; i++) {
    const fns = effectFn.deps[i]
    fns.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn()
  }
  effectFn.deps = [] 
  effectFn()
}

const objsMap = new WeakMap()
const data = { text: 'hello vue', ok: true }
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    trigger(target, key)
    return true
  }
})

function track(target, key) {
  if(!activeEffect) return
  let propsMap = objsMap.get(target)
  if(!propsMap) {
    objsMap.set(target, (propsMap = new Map()))
  }
  let fns = propsMap.get(key)
  if(!fns) {
    propsMap.set(key, (fns = new Set()))
  }
  fns.add(activeEffect)
  activeEffect.deps.push(fns) 
}

function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  const otherFns = new Set(fns) // (1)
  otherFns.forEach(fn => fn())
}

function fn() {
  document.body.innerText = obj.ok ? obj.text : 'ops...'
  console.log('Done!')
}
effect(fn)

在(1)處我們新增了一個otherFns,復(fù)制了fns用來遍歷。讓我們再來看看結(jié)果。

①處,更改obj.ok的值為false,改變了頁面的顯示,沒有導(dǎo)致死循環(huán)。②處,當(dāng)obj.okfalse時,副作用函數(shù)沒有執(zhí)行。至此,我們完成了針對分支切換場景下的優(yōu)化。

副作用函數(shù)嵌套產(chǎn)生的BUG

我們繼續(xù)從功能角度考慮,前面我們的副作用函數(shù)還是不夠復(fù)雜,實(shí)際應(yīng)用中(如組件嵌套渲染),副作用函數(shù)是可以發(fā)生嵌套的。

我們舉個簡單的嵌套示例:

let t1, t2
effect(function effectFn1() {
  console.log('effectFn1')
  effect(function effectFn2() {
    console.log('effectFn2')
    t2 = obj.bar
  })
  t1 = obj.foo
})

這段代碼中,我們將effectFn2嵌入了effectFn1中,將obj.foo賦值給t1,obj.bar賦值給t2。從響應(yīng)式的功能上看,如果我們修改obj.foo的值,應(yīng)該會觸發(fā)effectFn1的執(zhí)行,且間接觸發(fā)effectFn2執(zhí)行。

修改obj.foo的值僅觸發(fā)了effectFn2的更新,這與我們的預(yù)期不符。既然是effect這里出了問題,讓我們再來過一遍effect部分的代碼,看看能不能發(fā)現(xiàn)點(diǎn)什么。

let activeEffect // (1)

function cleanup(effectFn) { 
  for(let i = 0; i < effectFn.deps.length; i++) {
    const fns = effectFn.deps[i]
    fns.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    fn() // (2)
  }
  effectFn.deps = [] 
  effectFn()
}

仔細(xì)思考后,不難發(fā)現(xiàn)問題所在。我們在(1)處定義了一個全局變量activeEffect用于副作用函數(shù)注冊,這意味著同一時刻,我們僅能注冊一個副作用函數(shù)。在(2)處執(zhí)行了fn,此時注意,在我們給出的副作用函數(shù)嵌套示例中,effectFn1是先執(zhí)行effectFn2,再執(zhí)行t1 = obj.foo。也就是說,此時activeEffect注冊的副作用函數(shù)已經(jīng)由effectFn1變?yōu)榱?code>effectFn2。因此,當(dāng)執(zhí)行到t1 = obj.foo時,track收集的activeEffect已經(jīng)是被effectFn2覆蓋過的。所以,修改obj.foo,trigger觸發(fā)的就是effectFn2了。

要解決這個問題也很簡單,既然后出現(xiàn)的要先被收集,后進(jìn)先出,用棧解決就好了。

let activeEffect
const effectStack = [] // (1)

function cleanup(effectFn) { 
  for(let i = 0; i < effectFn.deps.length; i++) {
    const fns = effectFn.deps[i]
    fns.delete(effectFn)
  }
  effectFn.deps.length = 0
}

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn 
    effectStack.push(effectFn)
    fn() // (2)
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.deps = [] 
  effectFn()
}

這段代碼中,我們在(1)處定義了一個棧effectStack。不管(2)處如何更改activeEffect的內(nèi)容,都會被effectStack[effectStack.length - 1]回滾到原先正確的副作用函數(shù)上。

運(yùn)行的結(jié)果和我們的預(yù)期一致,到此為止,我們已經(jīng)完成了對嵌套副作用函數(shù)的處理。

自增/自減操作產(chǎn)生的BUG

這里還存在一個隱蔽的BUG,還和之前一樣,我們修改effect。

effect(() => obj.foo++)

很簡單的副作用函數(shù),這會有什么問題呢?執(zhí)行一下看看。

很不幸,棧溢出了。這個副作用函數(shù)僅包含一個obj.foo++,所以可以確定,棧溢出就是由這個自增運(yùn)算引起的。接下來的問題就是,這么簡單的自增操作,怎么會引起棧溢出呢?為了更好的說明問題,讓我們先來拆解問題。

effect(() => obj.foo = obj.foo + 1)

這段代碼中obj.foo = obj.foo + 1就等價于obj.foo++。這樣拆開之后問題一下就清楚了。這里同時進(jìn)行了obj.foogetset操作。先讀取obj.foo,收集了副作用函數(shù),再設(shè)置obj.foo,觸發(fā)了副作用函數(shù),而這個副作用函數(shù)中obj.foo又要被讀取,如此往復(fù),產(chǎn)生了死循環(huán)。為了驗(yàn)證這一點(diǎn),我們打印執(zhí)行的副作用函數(shù)。

上面的打印結(jié)果印證了我們的想法。造成這個BUG的主要原因是,當(dāng)getset操作同時存在時,我們收集和觸發(fā)的都是同一個副作用函數(shù)。這里我們只需要添加一個守衛(wèi)條件:當(dāng)觸發(fā)的副作用函數(shù)正在被執(zhí)行時,該副作用函數(shù)則不必再被執(zhí)行。

function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  const otherFns = new Set()
  fns && fns.forEach(fn => {
    if(fn !== activeEffect) { // (1)
      otherFns.add(fn)
    }
  })
  otherFns.forEach(fn => fn())
}

如此一來,相同的副作用函數(shù)僅會被觸發(fā)一次,避免了產(chǎn)生死循環(huán)。最后,我們驗(yàn)證一下即可。

到此這篇關(guān)于Vue3響應(yīng)式對象是如何實(shí)現(xiàn)的的文章就介紹到這了,更多相關(guān)Vue3響應(yīng)式對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論