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

手寫?Vue3?響應(yīng)式系統(tǒng)(核心就一個(gè)數(shù)據(jù)結(jié)構(gòu))

 更新時(shí)間:2022年06月23日 09:28:31   作者:??zxg_神說(shuō)要有光????  
這篇文章主要介紹了手寫?Vue3?響應(yīng)式系統(tǒng)(核心就一個(gè)數(shù)據(jù)結(jié)構(gòu)),響應(yīng)式就是被觀察的數(shù)據(jù)變化的時(shí)候做一系列聯(lián)動(dòng)處理。就像一個(gè)社會(huì)熱點(diǎn)事件,當(dāng)它有消息更新的時(shí)候,各方媒體都會(huì)跟進(jìn)做相關(guān)報(bào)道。這里社會(huì)熱點(diǎn)事件就是被觀察的目標(biāo)

前言

響應(yīng)式是 Vue 的特色,如果你簡(jiǎn)歷里寫了 Vue 項(xiàng)目,那基本都會(huì)問(wèn)響應(yīng)式實(shí)現(xiàn)原理。而且不只是 Vue,狀態(tài)管理庫(kù) Mobx 也是基于響應(yīng)式實(shí)現(xiàn)的。那響應(yīng)式是具體怎么實(shí)現(xiàn)的呢?與其空談原理,不如讓我們來(lái)手寫一個(gè)簡(jiǎn)易版吧。

響應(yīng)式

首先,什么是響應(yīng)式呢?

響應(yīng)式就是被觀察的數(shù)據(jù)變化的時(shí)候做一系列聯(lián)動(dòng)處理。就像一個(gè)社會(huì)熱點(diǎn)事件,當(dāng)它有消息更新的時(shí)候,各方媒體都會(huì)跟進(jìn)做相關(guān)報(bào)道。這里社會(huì)熱點(diǎn)事件就是被觀察的目標(biāo)。那在前端框架里,這個(gè)被觀察的目標(biāo)是什么呢?很明顯,是狀態(tài)。狀態(tài)一般是多個(gè),會(huì)通過(guò)對(duì)象的方式來(lái)組織。所以,我們觀察狀態(tài)對(duì)象的每個(gè) key 的變化,聯(lián)動(dòng)做一系列處理就可以了。

我們要維護(hù)這樣的數(shù)據(jù)結(jié)構(gòu):

狀態(tài)對(duì)象的每個(gè) key 都有關(guān)聯(lián)的一系列 effect 副作用函數(shù),也就是變化的時(shí)候聯(lián)動(dòng)執(zhí)行的邏輯,通過(guò) Set 來(lái)組織。

每個(gè) key 都是這樣關(guān)聯(lián)了一系列 effect 函數(shù),那多個(gè) key 就可以放到一個(gè) Map 里維護(hù)。

這個(gè) Map 是在對(duì)象存在的時(shí)候它就存在,對(duì)象銷毀的時(shí)候它也要跟著銷毀。(因?yàn)閷?duì)象都沒(méi)了自然也不需要維護(hù)每個(gè) key 關(guān)聯(lián)的 effect 了)

而 WeakMap 正好就有這樣的特性,WeakMap 的 key 必須是一個(gè)對(duì)象,value 可以是任意數(shù)據(jù),key 的對(duì)象銷毀的時(shí)候,value 也會(huì)銷毀。

所以,響應(yīng)式的 Map 會(huì)用 WeakMap 來(lái)保存,key 為原對(duì)象。

這個(gè)數(shù)據(jù)結(jié)構(gòu)就是響應(yīng)式的核心數(shù)據(jù)結(jié)構(gòu)了。

比如這樣的狀態(tài)對(duì)象:

const obj = {
    a: 1,
    b: 2
}

它的響應(yīng)式數(shù)據(jù)結(jié)構(gòu)就是這樣的:

const depsMap = new Map();
const aDeps = new Set();
depsMap.set('a', aDeps);
const bDeps = new Set();
depsMap.set('b', bDeps);
const reactiveMap = new WeakMap()
reactiveMap.set(obj, depsMap);

創(chuàng)建出的數(shù)據(jù)結(jié)構(gòu)就是圖中的那個(gè):

然后添加 deps 依賴,比如一個(gè)函數(shù)依賴了 a,那就要添加到 a 的 deps 集合里:

effect(() => {
    console.log(obj.a);
});

也就是這樣:

const depsMap = reactiveMap.get(obj);
const aDeps = depsMap.get('a');
aDeps.add(該函數(shù));

這樣維護(hù) deps 功能上沒(méi)啥問(wèn)題,但是難道要讓用戶手動(dòng)添加 deps 么?那不但會(huì)侵入業(yè)務(wù)代碼,而且還容易遺漏。

所以肯定不會(huì)讓用戶手動(dòng)維護(hù) deps,而是要做自動(dòng)的依賴收集。那怎么自動(dòng)收集依賴呢?讀取狀態(tài)值的時(shí)候,就建立了和該狀態(tài)的依賴關(guān)系,所以很容易想到可以代理狀態(tài)的 get 來(lái)實(shí)現(xiàn)。通過(guò) Object.defineProperty 或者 Proxy 都可以:

const data = {
    a: 1,
    b: 2
}
let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj);
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        
        let deps = depsMap.get(key)
        
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)

        return targetObj[key]
   }
})

effect 會(huì)執(zhí)行傳入的回調(diào)函數(shù) fn,當(dāng)你在 fn 里讀取 obj.a 的時(shí)候,就會(huì)觸發(fā) get,會(huì)拿到對(duì)象的響應(yīng)式的 Map,從里面取出 a 對(duì)應(yīng)的 deps 集合,往里面添加當(dāng)前的 effect 函數(shù)。

這樣就完成了一次依賴收集。

當(dāng)你修改 obj.a 的時(shí)候,要通知所有的 deps,所以還要代理 set:

set(targetObj, key, newVal) {
    targetObj[key] = newVal
    const depsMap = reactiveMap.get(targetObj)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
}

基本的響應(yīng)式完成了,我們測(cè)試一下:

打印了兩次,第一次是 1,第二次是 3。effect 會(huì)先執(zhí)行一次傳入的回調(diào)函數(shù),觸發(fā) get 來(lái)收集依賴,這時(shí)候打印的 obj.a 是 1然后當(dāng) obj.a 賦值為 3 后,會(huì)觸發(fā) set,執(zhí)行收集的依賴,這時(shí)候打印 obj.a 是 3

依賴也正確收集到了:

結(jié)果是對(duì)的,我們完成了基本的響應(yīng)式!當(dāng)然,響應(yīng)式不會(huì)只有這么點(diǎn)代碼的,我們現(xiàn)在的實(shí)現(xiàn)還不完善,還有一些問(wèn)題。比如,如果代碼里有分支切換,上次執(zhí)行會(huì)依賴 obj.b 下次執(zhí)行又不依賴了,這時(shí)候是不是就有了無(wú)效的依賴?

這樣一段代碼:

const obj = {
    a: 1,
    b: 2
}
effect(() => {
    console.log(obj.a ? obj.b : 'nothing');
});
obj.a = undefined;
obj.b = 3;

第一次執(zhí)行 effect 函數(shù),obj.a 是 1,這時(shí)候會(huì)走到第一個(gè)分支,又依賴了 obj.b。把 obj.a 修改為 undefined,觸發(fā) set,執(zhí)行所有的依賴函數(shù),這時(shí)候走到分支二,不再依賴 obj.b。

把 obj.b 修改為 3,按理說(shuō)這時(shí)候沒(méi)有依賴 b 的函數(shù)了,我們執(zhí)行試一下:

第一次打印 2 是對(duì)的,也就是走到了第一個(gè)分支,打印 obj.b

第二次打印 nothing 也是對(duì)的,這時(shí)候走到第二個(gè)分支。但是第三次打印 nothing 就不對(duì)了,因?yàn)檫@時(shí)候 obj.b 已經(jīng)沒(méi)有依賴函數(shù)了,但是還是打印了。

打印看下 deps,會(huì)發(fā)現(xiàn) obj.b 的 deps 沒(méi)有清除

所以解決方案就是每次添加依賴前清空下上次的 deps。怎么清空某個(gè)函數(shù)關(guān)聯(lián)的所有 deps 呢?記錄下就好了。

我們改造下現(xiàn)有的 effect 函數(shù):

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

記錄下這個(gè) effect 函數(shù)被放到了哪些 deps 集合里。也就是:

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

對(duì)之前的 fn 包一層,在函數(shù)上添加個(gè) deps 數(shù)組來(lái)記錄被添加到哪些依賴集合里。

get 收集依賴的時(shí)候,也記錄一份到這里:

這樣下次再執(zhí)行這個(gè) effect 函數(shù)的時(shí)候,就可以把這個(gè) effect 函數(shù)從上次添加到的依賴集合里刪掉:

cleanup 實(shí)現(xiàn)如下:

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

effectFn.deps 數(shù)組記錄了被添加到的 deps 集合,從中刪掉自己。全刪完之后就把上次記錄的 deps 數(shù)組置空。

我們?cè)賮?lái)測(cè)試下:

無(wú)限循環(huán)打印了,什么鬼?

問(wèn)題出現(xiàn)在這里:

set 的時(shí)候會(huì)執(zhí)行所有的當(dāng)前 key 的 deps 集合里的 effect 函數(shù)。

而我們執(zhí)行 effect 函數(shù)之前會(huì)把它從之前的 deps 集合中清掉:

執(zhí)行的時(shí)候又被添加到了 deps 集合。這樣 delete 又 add,delete 又 add,所以就無(wú)限循環(huán)了。

解決的方式就是創(chuàng)建第二個(gè) Set,只用于遍歷:

這樣就不會(huì)無(wú)限循環(huán)了。

再測(cè)試一次:

現(xiàn)在當(dāng) obj.a 賦值為 undefined 之后,再次執(zhí)行 effect 函數(shù),obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不會(huì)打印啥。

看下現(xiàn)在的響應(yīng)式數(shù)據(jù)結(jié)構(gòu):

確實(shí),b 的 deps 集合被清空了。那現(xiàn)在的響應(yīng)式實(shí)現(xiàn)是完善的了么?也不是,還有一個(gè)問(wèn)題:

如果 effect 嵌套了,那依賴還能正確的收集么?

首先講下為什么要支持 effect 嵌套,因?yàn)榻M件是可以嵌套的,而且組件里會(huì)寫 effect,那也就是 effect 嵌套了,所以必須支持嵌套。

我們嵌套下試試:

effect(() => {
    console.log('effect1');
    effect(() => {
        console.log('effect2');
        obj.b;
    });
    obj.a;
});
obj.a = 3;

按理說(shuō)會(huì)打印一次 effect1、一次 effect2,這是最開始的那次執(zhí)行。然后 obj.a 修改為 3 后,會(huì)觸發(fā)一次 effect1 的打印,執(zhí)行內(nèi)層 effect,又觸發(fā)一次 effect2 的打印。也就是會(huì)打印 effect1、effect2、effect1、effect2。

我們測(cè)試下:

打印了 effect1、effet2 這是對(duì)的,但第三次打印的是 effect2,這說(shuō)明 obj.a 修改后并沒(méi)有執(zhí)行外層函數(shù),而是執(zhí)行的內(nèi)層函數(shù)。為什么呢?

看下這段代碼:

我們執(zhí)行 effect 的時(shí)候,會(huì)把它賦值給一個(gè)全局變量 activeEffect,然后后面收集依賴就用的這個(gè)。

當(dāng)嵌套 effect 的時(shí)候,內(nèi)層函數(shù)執(zhí)行后會(huì)修改 activeEffect 這樣收集到的依賴就不對(duì)了。

怎么辦呢?嵌套的話加一個(gè)棧來(lái)記錄 effect 不就行了?

也就是這樣:

執(zhí)行 effect 函數(shù)前把當(dāng)前 effectFn 入棧,執(zhí)行完以后出棧,修改 activeEffect 為棧頂?shù)?effectFn。

這樣就保證了收集到的依賴是正確的。

這種思想的應(yīng)用還是很多的,需要保存和恢復(fù)上下文的時(shí)候,都是這樣加一個(gè)棧。

我們?cè)贉y(cè)試一下:

現(xiàn)在的打印就對(duì)了。至此,我們的響應(yīng)式系統(tǒng)就算比較完善了。

全部代碼如下:

const data = {
    a: 1,
    b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
  const effectFn = () => {
      cleanup(effectFn)
      activeEffect = effectFn
      effectStack.push(effectFn);
      fn()
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = []
  effectFn()
}
function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
    get(targetObj, key) {
        let depsMap = reactiveMap.get(targetObj)
        
        if (!depsMap) {
          reactiveMap.set(targetObj, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
          depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
        activeEffect.deps.push(deps);
        return targetObj[key]
   },
   set(targetObj, key, newVal) {
        targetObj[key] = newVal
        const depsMap = reactiveMap.get(targetObj)
        if (!depsMap) return
        const effects = depsMap.get(key)
        // effects && effects.forEach(fn => fn())
        const effectsToRun = new Set(effects);
        effectsToRun.forEach(effectFn => effectFn());
    }
})

總結(jié)

響應(yīng)式就是數(shù)據(jù)變化的時(shí)候做一系列聯(lián)動(dòng)的處理。

核心是這樣一個(gè)數(shù)據(jù)結(jié)構(gòu):

最外層是 WeakMap,key 為對(duì)象,value 為響應(yīng)式的 Map。這樣當(dāng)對(duì)象銷毀時(shí),Map 也會(huì)銷毀。Map 里保存了每個(gè) key 的依賴集合,用 Set 組織。

我們通過(guò) Proxy 來(lái)完成自動(dòng)的依賴收集,也就是添加 effect 到對(duì)應(yīng) key 的 deps 的集合里。 set 的時(shí)候觸發(fā)所有的 effect 函數(shù)執(zhí)行。

這就是基本的響應(yīng)式系統(tǒng)。

但是還不夠完善,每次執(zhí)行 effect 前要從上次添加到的 deps 集合中刪掉它,然后重新收集依賴。這樣可以避免因?yàn)榉种袚Q產(chǎn)生的無(wú)效依賴。并且執(zhí)行 deps 中的 effect 前要?jiǎng)?chuàng)建一個(gè)新的 Set 來(lái)執(zhí)行,避免 add、delete 循環(huán)起來(lái)。此外,為了支持嵌套 effect,需要在執(zhí)行 effect 之前把它推到棧里,然后執(zhí)行完出棧。解決了這幾個(gè)問(wèn)題之后,就是一個(gè)完善的 Vue 響應(yīng)式系統(tǒng)了。當(dāng)然,現(xiàn)在雖然功能是完善的,但是沒(méi)有實(shí)現(xiàn) computed、watch 等功能,之后再實(shí)現(xiàn)。

最后,再來(lái)看一下這個(gè)數(shù)據(jù)結(jié)構(gòu),理解了它就理解了 vue 響應(yīng)式的核心:

到此這篇關(guān)于手寫 Vue3 響應(yīng)式系統(tǒng)(核心就一個(gè)數(shù)據(jù)結(jié)構(gòu))的文章就介紹到這了,更多相關(guān)Vue3 響應(yīng)式系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue開發(fā)之封裝分頁(yè)組件與使用示例

    Vue開發(fā)之封裝分頁(yè)組件與使用示例

    這篇文章主要介紹了Vue開發(fā)之封裝分頁(yè)組件與使用,結(jié)合實(shí)例形式分析了vue.js封裝分頁(yè)組件操作以及使用組件進(jìn)行分頁(yè)的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2019-04-04
  • vue中npm如何設(shè)置倉(cāng)庫(kù)地址

    vue中npm如何設(shè)置倉(cāng)庫(kù)地址

    在使用npm命令時(shí),如果直接從國(guó)外的倉(cāng)庫(kù)下載依賴,下載速度很慢,甚至?xí)螺d不下來(lái),我們可以更換npm的倉(cāng)庫(kù)源,提高下載速度,這篇文章主要給大家介紹了關(guān)于vue中npm如何設(shè)置倉(cāng)庫(kù)地址的相關(guān)資料,需要的朋友可以參考下
    2023-12-12
  • vue百度地圖 + 定位的詳解

    vue百度地圖 + 定位的詳解

    這篇文章主要介紹了vue百度地圖 + 定位的詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • vue2基本響應(yīng)式實(shí)現(xiàn)方式之讓數(shù)組也變成響應(yīng)式

    vue2基本響應(yīng)式實(shí)現(xiàn)方式之讓數(shù)組也變成響應(yīng)式

    這篇文章主要介紹了vue2基本響應(yīng)式實(shí)現(xiàn)方式之讓數(shù)組也變成響應(yīng)式問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • vue.js內(nèi)部自定義指令與全局自定義指令的實(shí)現(xiàn)詳解(利用directive)

    vue.js內(nèi)部自定義指令與全局自定義指令的實(shí)現(xiàn)詳解(利用directive)

    這篇文章主要給大家介紹了關(guān)于vue.js內(nèi)部自定義指令與全局自定義指令的實(shí)現(xiàn)方法,vue.js中實(shí)現(xiàn)自定義指令的主要是利用directive,directive這個(gè)單詞是我們寫自定義指令的關(guān)鍵字,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-07-07
  • vue3點(diǎn)擊按鈕下載文件功能的代碼實(shí)現(xiàn)

    vue3點(diǎn)擊按鈕下載文件功能的代碼實(shí)現(xiàn)

    在寫vue項(xiàng)目時(shí),有個(gè)需求是點(diǎn)擊表格中某一行的下載按鈕,然后開始下載這一行對(duì)應(yīng)的文件,所以本文小編給大家介紹了使用vue3實(shí)現(xiàn)點(diǎn)擊按鈕下載文件功能,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下
    2024-01-01
  • 使用Vue+MySQL實(shí)現(xiàn)登錄注冊(cè)的實(shí)戰(zhàn)案例

    使用Vue+MySQL實(shí)現(xiàn)登錄注冊(cè)的實(shí)戰(zhàn)案例

    第一次用Vue+MySQL實(shí)現(xiàn)注冊(cè)登錄功能,就已經(jīng)踩了很多坑,下面這篇文章主要給大家介紹了關(guān)于使用Vue+MySQL實(shí)現(xiàn)登錄注冊(cè)案例的相關(guān)資料,需要的朋友可以參考下
    2022-05-05
  • vue 輸入框輸入任意內(nèi)容返回?cái)?shù)字的實(shí)現(xiàn)

    vue 輸入框輸入任意內(nèi)容返回?cái)?shù)字的實(shí)現(xiàn)

    本文主要介紹了vue 輸入框輸入任意內(nèi)容返回?cái)?shù)字的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-03-03
  • 教你在vue項(xiàng)目中使用svg圖標(biāo)的方法

    教你在vue項(xiàng)目中使用svg圖標(biāo)的方法

    本文給大家介紹了在vue項(xiàng)目中使用svg圖標(biāo)的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2022-04-04
  • 淺談Vue SSR中的Bundle的具有使用

    淺談Vue SSR中的Bundle的具有使用

    這篇文章主要介紹了淺談Vue SSR中的Bundle的具有使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11

最新評(píng)論