中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么手寫Vue3響應式系統

發布時間:2022-06-23 14:08:45 來源:億速云 閱讀:176 作者:iii 欄目:開發技術

這篇文章主要介紹了怎么手寫Vue3響應式系統的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇怎么手寫Vue3響應式系統文章都會有所收獲,下面我們一起來看看吧。

響應式

首先,什么是響應式呢?

響應式就是被觀察的數據變化的時候做一系列聯動處理。就像一個社會熱點事件,當它有消息更新的時候,各方媒體都會跟進做相關報道。這里社會熱點事件就是被觀察的目標。那在前端框架里,這個被觀察的目標是什么呢?很明顯,是狀態。狀態一般是多個,會通過對象的方式來組織。所以,我們觀察狀態對象的每個 key 的變化,聯動做一系列處理就可以了。

我們要維護這樣的數據結構:

怎么手寫Vue3響應式系統

狀態對象的每個 key 都有關聯的一系列 effect 副作用函數,也就是變化的時候聯動執行的邏輯,通過 Set 來組織。

每個 key 都是這樣關聯了一系列 effect 函數,那多個 key 就可以放到一個 Map 里維護。

這個 Map 是在對象存在的時候它就存在,對象銷毀的時候它也要跟著銷毀。(因為對象都沒了自然也不需要維護每個 key 關聯的 effect 了)

而 WeakMap 正好就有這樣的特性,WeakMap 的 key 必須是一個對象,value 可以是任意數據,key 的對象銷毀的時候,value 也會銷毀。

所以,響應式的 Map 會用 WeakMap 來保存,key 為原對象。

這個數據結構就是響應式的核心數據結構了。

比如這樣的狀態對象:

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

它的響應式數據結構就是這樣的:

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);

創建出的數據結構就是圖中的那個:

怎么手寫Vue3響應式系統

怎么手寫Vue3響應式系統

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

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

也就是這樣:

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

這樣維護 deps 功能上沒啥問題,但是難道要讓用戶手動添加 deps 么?那不但會侵入業務代碼,而且還容易遺漏。

所以肯定不會讓用戶手動維護 deps,而是要做自動的依賴收集。那怎么自動收集依賴呢?讀取狀態值的時候,就建立了和該狀態的依賴關系,所以很容易想到可以代理狀態的 get 來實現。通過 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 會執行傳入的回調函數 fn,當你在 fn 里讀取 obj.a 的時候,就會觸發 get,會拿到對象的響應式的 Map,從里面取出 a 對應的 deps 集合,往里面添加當前的 effect 函數。

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

當你修改 obj.a 的時候,要通知所有的 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())
}

基本的響應式完成了,我們測試一下:

怎么手寫Vue3響應式系統

打印了兩次,第一次是 1,第二次是 3。effect 會先執行一次傳入的回調函數,觸發 get 來收集依賴,這時候打印的 obj.a 是 1然后當 obj.a 賦值為 3 后,會觸發 set,執行收集的依賴,這時候打印 obj.a 是 3

依賴也正確收集到了:

怎么手寫Vue3響應式系統

結果是對的,我們完成了基本的響應式!當然,響應式不會只有這么點代碼的,我們現在的實現還不完善,還有一些問題。比如,如果代碼里有分支切換,上次執行會依賴 obj.b 下次執行又不依賴了,這時候是不是就有了無效的依賴?

這樣一段代碼:

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

第一次執行 effect 函數,obj.a 是 1,這時候會走到第一個分支,又依賴了 obj.b。把 obj.a 修改為 undefined,觸發 set,執行所有的依賴函數,這時候走到分支二,不再依賴 obj.b。

把 obj.b 修改為 3,按理說這時候沒有依賴 b 的函數了,我們執行試一下:

怎么手寫Vue3響應式系統

第一次打印 2 是對的,也就是走到了第一個分支,打印 obj.b

第二次打印 nothing 也是對的,這時候走到第二個分支。但是第三次打印 nothing 就不對了,因為這時候 obj.b 已經沒有依賴函數了,但是還是打印了。

打印看下 deps,會發現 obj.b 的 deps 沒有清除

怎么手寫Vue3響應式系統

所以解決方案就是每次添加依賴前清空下上次的 deps。怎么清空某個函數關聯的所有 deps 呢?記錄下就好了。

我們改造下現有的 effect 函數:

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

記錄下這個 effect 函數被放到了哪些 deps 集合里。也就是:

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

對之前的 fn 包一層,在函數上添加個 deps 數組來記錄被添加到哪些依賴集合里。

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

怎么手寫Vue3響應式系統

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

怎么手寫Vue3響應式系統

cleanup 實現如下:

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 數組記錄了被添加到的 deps 集合,從中刪掉自己。全刪完之后就把上次記錄的 deps 數組置空。

我們再來測試下:

怎么手寫Vue3響應式系統

無限循環打印了,什么鬼?

問題出現在這里:

怎么手寫Vue3響應式系統

set 的時候會執行所有的當前 key 的 deps 集合里的 effect 函數。

而我們執行 effect 函數之前會把它從之前的 deps 集合中清掉:

怎么手寫Vue3響應式系統

執行的時候又被添加到了 deps 集合。這樣 delete 又 add,delete 又 add,所以就無限循環了。

解決的方式就是創建第二個 Set,只用于遍歷:

怎么手寫Vue3響應式系統

這樣就不會無限循環了。

再測試一次:

怎么手寫Vue3響應式系統

現在當 obj.a 賦值為 undefined 之后,再次執行 effect 函數,obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不會打印啥。

看下現在的響應式數據結構:

怎么手寫Vue3響應式系統

確實,b 的 deps 集合被清空了。那現在的響應式實現是完善的了么?也不是,還有一個問題:

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

首先講下為什么要支持 effect 嵌套,因為組件是可以嵌套的,而且組件里會寫 effect,那也就是 effect 嵌套了,所以必須支持嵌套。

我們嵌套下試試:

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

按理說會打印一次 effect1、一次 effect2,這是最開始的那次執行。然后 obj.a 修改為 3 后,會觸發一次 effect1 的打印,執行內層 effect,又觸發一次 effect2 的打印。也就是會打印 effect1、effect2、effect1、effect2。

我們測試下:

怎么手寫Vue3響應式系統

打印了 effect1、effet2 這是對的,但第三次打印的是 effect2,這說明 obj.a 修改后并沒有執行外層函數,而是執行的內層函數。為什么呢?

看下這段代碼:

怎么手寫Vue3響應式系統

我們執行 effect 的時候,會把它賦值給一個全局變量 activeEffect,然后后面收集依賴就用的這個。

當嵌套 effect 的時候,內層函數執行后會修改 activeEffect 這樣收集到的依賴就不對了。

怎么辦呢?嵌套的話加一個棧來記錄 effect 不就行了?

也就是這樣:

怎么手寫Vue3響應式系統

執行 effect 函數前把當前 effectFn 入棧,執行完以后出棧,修改 activeEffect 為棧頂的 effectFn。

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

這種思想的應用還是很多的,需要保存和恢復上下文的時候,都是這樣加一個棧。

我們再測試一下:

怎么手寫Vue3響應式系統

現在的打印就對了。至此,我們的響應式系統就算比較完善了。

全部代碼如下:

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());
    }
})

總結

響應式就是數據變化的時候做一系列聯動的處理。

核心是這樣一個數據結構:

怎么手寫Vue3響應式系統

最外層是 WeakMap,key 為對象,value 為響應式的 Map。這樣當對象銷毀時,Map 也會銷毀。Map 里保存了每個 key 的依賴集合,用 Set 組織。

我們通過 Proxy 來完成自動的依賴收集,也就是添加 effect 到對應 key 的 deps 的集合里。 set 的時候觸發所有的 effect 函數執行。

這就是基本的響應式系統。

但是還不夠完善,每次執行 effect 前要從上次添加到的 deps 集合中刪掉它,然后重新收集依賴。這樣可以避免因為分支切換產生的無效依賴。并且執行 deps 中的 effect 前要創建一個新的 Set 來執行,避免 add、delete 循環起來。此外,為了支持嵌套 effect,需要在執行 effect 之前把它推到棧里,然后執行完出棧。解決了這幾個問題之后,就是一個完善的 Vue 響應式系統了。當然,現在雖然功能是完善的,但是沒有實現 computed、watch 等功能,之后再實現。

最后,再來看一下這個數據結構,理解了它就理解了 vue 響應式的核心:

怎么手寫Vue3響應式系統

關于“怎么手寫Vue3響應式系統”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“怎么手寫Vue3響應式系統”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

宁阳县| 长兴县| 甘孜县| 遵化市| 原平市| 朔州市| 泸水县| 资阳市| 宣城市| 黄大仙区| 黄浦区| 伊通| 中山市| 南开区| 扎鲁特旗| 华宁县| 淮滨县| 榆林市| 陆丰市| 都匀市| 泰兴市| 中牟县| 祥云县| 敖汉旗| 浦东新区| 自贡市| 龙陵县| 凉山| 夏河县| 镇远县| 延安市| 新蔡县| 周宁县| 二连浩特市| 图片| 延川县| 昭苏县| 和平县| 肇州县| 芒康县| 斗六市|