最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3響應式對象是如何實現(xiàn)的(2)

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

前言

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

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

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

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)處給響應式對象新增加了一個boolean類型的屬性ok,在(2)處我們利用ok的真值,來選擇將誰賦值給document.body.innerText?,F(xiàn)在,我們將obj.ok的值置為false,這就意味著,document.body.innerText的值不再依賴于obj.text,而直接取字符串'ops...'。

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

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

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

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

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

這就要求我們能夠在每次執(zhí)行副作用函數(shù)前,將該副作用函數(shù)從相關的副作用函數(shù)收集器中刪除,再重新建立聯(lián)系。為了實現(xià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ù)。

看上去好像沒啥問題了,但是運行代碼會發(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)
  }

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

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

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

在上例中,我們復制了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,復制了fns用來遍歷。讓我們再來看看結(jié)果。

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

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

我們繼續(xù)從功能角度考慮,前面我們的副作用函數(shù)還是不夠復雜,實際應用中(如組件嵌套渲染),副作用函數(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。從響應式的功能上看,如果我們修改obj.foo的值,應該會觸發(fā)effectFn1的執(zhí)行,且間接觸發(fā)effectFn2執(zhí)行。

修改obj.foo的值僅觸發(fā)了effectFn2的更新,這與我們的預期不符。既然是effect這里出了問題,讓我們再來過一遍effect部分的代碼,看看能不能發(fā)現(xià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()
}

仔細思考后,不難發(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。因此,當執(zhí)行到t1 = obj.foo時,track收集的activeEffect已經(jīng)是被effectFn2覆蓋過的。所以,修改obj.foo,trigger觸發(fā)的就是effectFn2了。

要解決這個問題也很簡單,既然后出現(xià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ù)上。

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

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

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

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

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

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

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

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

上面的打印結(jié)果印證了我們的想法。造成這個BUG的主要原因是,當getset操作同時存在時,我們收集和觸發(fā)的都是同一個副作用函數(shù)。這里我們只需要添加一個守衛(wèi)條件:當觸發(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)。最后,我們驗證一下即可。

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

相關文章

  • vue為自定義路徑設置別名的方法

    vue為自定義路徑設置別名的方法

    這篇文章介紹了vue為自定義路徑設置別名的方法,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-11-11
  • Vue使用vue-recoure + http-proxy-middleware + vuex配合promise實現(xiàn)基本的跨域請求封裝

    Vue使用vue-recoure + http-proxy-middleware + vuex配合promise實現(xiàn)基本

    這篇文章主要介紹了Vue使用vue-recoure + http-proxy-middleware + vuex配合promise實現(xiàn)基本的跨域請求封裝問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-10-10
  • vue基于兩個計算屬性實現(xiàn)選中和全選功能示例

    vue基于兩個計算屬性實現(xiàn)選中和全選功能示例

    這篇文章主要介紹了vue基于兩個計算屬性實現(xiàn)選中和全選功能,結(jié)合實例形式分析了vue計算屬性get及set操作頁面元素實現(xiàn)選中與全選功能相關操作技巧,需要的朋友可以參考下
    2019-02-02
  • vue:axios請求本地json路徑錯誤問題及解決

    vue:axios請求本地json路徑錯誤問題及解決

    這篇文章主要介紹了vue:axios請求本地json路徑錯誤問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • vue3.2最新語法使用socket.io實現(xiàn)即時通訊詳解

    vue3.2最新語法使用socket.io實現(xiàn)即時通訊詳解

    這篇文章主要為大家介紹了vue3.2最新語法使用socket.io實現(xiàn)即時通訊詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-06-06
  • vue3中展示markdown格式文章的三種形式

    vue3中展示markdown格式文章的三種形式

    這篇文章主要為大家詳細介紹了vue3中展示markdown格式文章的三種形式,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下
    2025-04-04
  • vue2.0如何借用vue-pdf實現(xiàn)在線預覽pdf文件

    vue2.0如何借用vue-pdf實現(xiàn)在線預覽pdf文件

    這篇文章主要介紹了vue2.0如何借用vue-pdf實現(xiàn)在線預覽pdf文件問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • 關于vue組件事件屬性穿透詳解

    關于vue組件事件屬性穿透詳解

    今天小編就為大家分享一篇關于vue組件事件屬性穿透詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-10-10
  • 解決Vue 給mapState中定義的屬性賦值報錯的問題

    解決Vue 給mapState中定義的屬性賦值報錯的問題

    這篇文章主要介紹了解決Vue 給mapState中定義的屬性賦值報錯的問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-06-06
  • 淺談Vue數(shù)據(jù)響應

    淺談Vue數(shù)據(jù)響應

    這篇文章主要介紹了淺談Vue數(shù)據(jù)響應,Vue的數(shù)據(jù)響應主要是依賴了Object.defineProperty(),下面就具體來介紹一下如何實現(xiàn)數(shù)據(jù)響應
    2018-11-11

最新評論

陈巴尔虎旗| 安义县| 新建县| 晋宁县| 麻江县| 闵行区| 鲜城| 岳西县| 深州市| 德江县| 通辽市| 常山县| 张家界市| 镇康县| 磴口县| 固始县| 镇坪县| 凤翔县| 连平县| 辽中县| 海盐县| 兰西县| 青田县| 亳州市| 东乌珠穆沁旗| 贺州市| 延安市| 华阴市| 兰考县| 海宁市| 通许县| 岚皋县| 丽江市| 甘洛县| 长沙县| 泾源县| 綦江县| 句容市| 五家渠市| 海宁市| 淅川县|