Vue響應式系統分支切換與cleanup - 清除遺留的副作用函數

文章目錄

  • 前言
  • 分支切換與cleanup
    • 分支切換的問題
    • 依賴集合的收集
    • cleanup的實現
    • 完整的代碼展示

前言

本篇文章代碼思路來自 Vue3.0 源碼, 部分理解來源于霍春陽 《Vue.js設計與實現》這本書的理解, 感興趣的小伙伴可以自行購買閱讀。可以非常明確的感受到作者對 Vue 的深刻理解以及用心, 富含非常全面的 Vue 的知識點, 強烈推薦給大家。

接上文
Vue響應式原理和本質 - 實現一個完善的響應式系統

分支切換與cleanup

前文回顧

在上一篇文章中, 我們實現了一個基本的響應式系統代碼如下, 實現過程可以根據自己的需要進行閱讀, 鏈接給到大家: http://lanan.blog.csdn.net/article/details/134127326。但是目前實現的響應式系統仍然存在一些問題, 本文針對分支切換產生的問題繼續完善響應式系統

// 前文已實現的響應式系統const data = { name: "chenyq", age: 18 };
const bucket = new WeakMap();
const obj = new Proxy(data, {get(target, key) {// 將副作用函數收集到桶中track(target, key);// 返回屬性值return target[key];},set(target, key, newVal) {// 設置屬性值target[key] = newVal;// 從桶中取出副作用函數執行trigger(target, key);},
});function track(target, key) {// 沒有activeEffect, 直接returnif (!activeEffect) return target[key];// 根據target從桶中取出depsMaplet depsMap = bucket.get(target);// 如果depsMap不存在, 那么就需要創建一個depsMap與之關聯if (!depsMap) bucket.set(target, (depsMap = new Map()));// 再根據key, 從depsMap中取出deps, deps是一個Set集合, 里面存放的是與當前key相關的所有副作用函數let deps = depsMap.get(key);// 如果deps不存在, 則創建一個deps, 并將其添加到depsMap中if (!deps) depsMap.set(key, (deps = new Set()));// 最后將當前激活的副作用函數添加到桶里deps.add(activeEffect);
}function trigger(target, key) {// 根據target從桶中取出depsMapconst depsMap = bucket.get(target);if (!depsMap) return;// 取出與key相關的副作用函數const effects = depsMap.get(key);// 執行副作用函數effects && effects.forEach((fn) => fn());
}let activeEffect;
function effect(fn) {activeEffect = fn;fn();
}// 測試部分
// 執行副作用函數, 觸發讀取
effect(() => {document.body.innerText = obj.name;
});// 1秒后對obj.name屬性進行修改
setTimeout(() => {obj.name = "abc";// obj.age = 19;// obj.notExist = "abc";
}, 1000);

分支切換的問題

首先, 我們需要知道分支切換會給我們上面實現的響應式系統帶來哪些問題, 如有下面一段代碼, effectFn 函數內部存在一個三元表達式, 當字段 obj.flag 發生變換時, 代碼所需要執行的分支也會跟隨變化。

const data = { flag: true, text: "分支切換問題" };
const obj = new Proxy(data, { /* ... */ });
function effect(fn) { /* ... */ }effect(function effectFn() {document.body.innerText = obj.flag ? obj.text : "not fount";
});

上面的分支切換會產生遺留的副作用函數, 例如上面代碼中, 當 obj.flag 為 true 的時候, 會讀取 obj.text 屬性的值, 此時就會觸發 flag 和 text 屬性的 get 操作, 對應的會跟蹤 flag 和 text 屬性所依賴的副作用函數 effectFn。如下所示, effectFn 分別被字段 flag 和 text 所對應的依賴集合進行收集:

data└── flag└── effectFn└── text└── effectFn

當我們將 obj.flag 的值修改為 false 時, 會觸發 obj.flag 的 set 操作, 會重新執行 effectFn 函數, 但由于此時 obj.flag 的值為 false , 不會讀取 obj.text 屬性。所以我們期望副作用函數 effectFn 不被 obj.text 對應的依賴集合進行收集, 此時副作用函數 effectFn 與響應式建立的關系如下:

data└── flag└── effectFn

但按照我們目前的實現, 無法做到這一點, 在我們將 obj.flag 修改為 false, 并重新執行了副作用函數 effectFn 后, 它們之間對應的依賴關系任然為之前的樣子, 這也就產生了副作用函數的遺留問題, 這個問題會導致不必要的更新, 如用下面代碼來舉例:

調用 effect 函數會執行一次 effectFn 函數, 打印一次 “effect is running”

const data = { flag: true, text: "分支切換問題" };
const obj = new Proxy(data, { /* ... */ });
function effect(fn) { /* ... */ }effect(function effectFn() {console.log("effect is running");document.body.innerText = obj.flag ? obj.text : "not fount";
});

我們將 obj.flag 修改為 false 后, 會觸發 obj.flag 的 set 操作, 即副作用函數 effectFn 會重新執行, 打印第二次 “effect is running”

setTimeout(() => {obj.flag = false;
}, 1000);

若此時我們繼續修改 obj.text, 可以發現打印了第三次 “effect is running”, 說明 obj.text 對應的依賴集合中, 仍然保留了 effectFn 函數

obj.text = "測試分支切換";

我們想要實現的是, 當 obj.flag 為 false 的時候, 由于此時不在讀取 obj.tex t屬性, 那么該情況下 obj.text 無論如何進行變換, 都不應該重新執行副作用函數 effectFn 。目前的情況就是, obj.flag 為 false 的時候, 只要修改了 obj.text 的值, 就會重新執行副作用函數 effectFn, 歸根結底還是因為副作用函數遺留產生的問題。

依賴集合的收集

其實解決這個問題的思路很簡單, 我們只需要每次執行副作用函數的時候, 把這個副作用函數從所有被關聯的依賴集合中刪除, 比如上文中的建立的對應關系, obj.flag 和 obj.text 都和副作用函數 effectFn 相關聯, 我們就可以在執行副作用函數 effectFn, 將它與 obj.flag 和 obj.text 的關系都斷開, 如下所示:

data└── flag└── text

待副作用函數執行完成之后, 我們再重新建立聯系, 這樣一來新建立的聯系中就不會包含遺留的副作用函數。那么現在的問題就是, 想要將一個副作用函數從所有與之關聯的依賴集合中移除, 我們需要明確的知道哪些依賴集合包含這個副作用函數。這樣的話我們就需要對 effect 函數進行重新設計, 我們在 effect 函數中, 定義一個新的函數 effectFn, 將定義的 effectFn 函數設置為當前激活的副作用函數, 并為這個 effectFn 函數添加 deps 屬性, deps 屬性為一個數組, 數組中存放當前副作用函數的依賴集合, 代碼如下所示:

let activeEffect;
function effect(fn) {// 定義一個effectFn函數const effectFn = () => {// 將定義的effectFn函數設置為當前激活的副作用函數activeEffect = effectFn;fn();};// deps屬性中用來存儲所有與該副作用函數相關聯的依賴集合effectFn.deps = [];effectFn();
}

那么我們只需要在 track 函數中, 將當前副作用函數的依賴集合添加到 effectFn.deps 數組中, 就可以完成副作用函數相關聯的依賴集合的收集, 如下代碼所示:

function track(target, key) {if (!activeEffect) return target[key];let depsMap = bucket.get(target);if (!depsMap) bucket.set(target, (depsMap = new Map()));let deps = depsMap.get(key);if (!deps) depsMap.set(key, (deps = new Set()));deps.add(activeEffect);// 將依賴集合收集到activeEffect.deps數組中activeEffect.deps.push(deps)
}

cleanup的實現

有了上面的依賴集合收集之后, 我們就可以做到每次在副作用函數執行時, 根據 effectFn.deps , 將副作用函數從所有依賴集合中移除:

let activeEffect;
function effect(fn) {const effectFn = () => {// 調用cleanup函數完成清除cleanup(effectFn)activeEffect = effectFn;fn();};effectFn.deps = [];effectFn();
}

cleanup 函數接收一個副作用函數作為參數, 遍歷 effectFn.deps 數組, 數組中每一項都是一個依賴集合, 依次從每一個依賴集合中將副作用函數 effectFn 移除, 實現代碼如下所示:

function cleanup(effectFn) {// 遍歷effectFn.deps數組const length = effectFn.deps.length;for (let i = 0; i < length; i++) {// 取出依賴集合const deps = effectFn.deps[i];// 將副作用函數從依賴集合中移除deps.delete(effectFn);}// 將effectFn.deps重置為空數組effectFn.deps.length = 0;
}

完成上面操作, 我們的響應式系統就已經可以避免副作用函數的遺留問題了, 但是上面代碼運行會出現一個無限循環, 導致死循環的原因就出現在 trigger 函數中:

function trigger(target, key) {const depsMap = bucket.get(target);if (!depsMap) return;const effects = depsMap.get(key);effects && effects.forEach((fn) => fn()); // 這行代碼有問題
}

我們來分析一下問題的原因, 在 trigger 內部, 我們遍歷的 effects, 它是一個 Set 集合, 里面存儲著副作用函數; 我們遍歷這個 Set 集合時, 會取出每一個副作用函數進行執行, 在執行副作用函數時會調用 cleanup 函數進行清除, 會將這個副作用函數從 Set 集合中移除; 但是我們繼續執行函數, 會導致它又重新被收集到 Set 集合當中, 而此時 Set 集合遍歷任在進行。

語言規范中對此有明確的說明: 在調用 forEach 遍歷 Set 集合時, 如果一個值已經被訪問過了, 但該值被刪除并重新添加到集合, 如果此時 forEach 遍歷沒有結束, 那么該值會重新被訪問。

例如下面代碼, Set 集合當中只有一個元素, 在遍歷時我們將這個元素先刪除, 再添加; 那么該值就會被重新訪問, 進而這個循環就會無限執行進行下去:

const set = new Set([1])set.forEach(item => {set.delete(1)set.add(1)
})

因此 trigger 中的代碼同理, 也會無限的執行下去。解決辦法很簡單, 我們可以構造另外一個 Set 集合并遍歷它, 這樣就不會無限的執行下去了。具體做法: 由于我們將副作用函數移除和添加的函數是 effects, 那么我們就不要直接遍歷 effects, 創建一個新的集合 effectsToRun 代替直接遍歷 effects, 代碼如下所示:

function trigger(target, key) {const depsMap = bucket.get(target);if (!depsMap) return;const effects = depsMap.get(key);// 創建一個新的Set集合用來遍歷const effectsToRun = new Set(effects)// 執行副作用函數effectsToRun.forEach(effectFn => effectFn())
}

完整的代碼展示

到這里我們就徹底解決了副作用函數產生遺留的問題, 到這里我們實現的響應式系統的完整代碼給到大家:

const data = { flag: true, text: "分支切換問題" };
const bucket = new WeakMap();
const obj = new Proxy(data, {get(target, key) {// 將副作用函數收集到桶中track(target, key);// 返回屬性值return target[key];},set(target, key, newVal) {// 設置屬性值target[key] = newVal;// 從桶中取出副作用函數執行trigger(target, key);},
});// 依賴收集
function track(target, key) {// 沒有activeEffect, 直接returnif (!activeEffect) return target[key];// 根據target從桶中取出depsMaplet depsMap = bucket.get(target);// 如果depsMap不存在, 那么就需要創建一個depsMap與之關聯if (!depsMap) bucket.set(target, (depsMap = new Map()));// 再根據key, 從depsMap中取出deps, deps是一個Set集合, 里面存放的是與當前key相關的所有副作用函數let deps = depsMap.get(key);// 如果deps不存在, 則創建一個deps, 并將其添加到depsMap中if (!deps) depsMap.set(key, (deps = new Set()));// 最后將當前激活的副作用函數添加到桶里deps.add(activeEffect);// 將依賴集合收集到activeEffect.deps數組中activeEffect.deps.push(deps);
}// 派發更新
function trigger(target, key) {// 根據target從桶中取出depsMapconst depsMap = bucket.get(target);if (!depsMap) return;// 取出與key相關的副作用函數const effects = depsMap.get(key);// 創建一個新的Set集合用來遍歷const effectsToRun = new Set(effects);// 執行副作用函數effectsToRun.forEach((effectFn) => effectFn());
}let activeEffect;
function effect(fn) {// 定義一個effectFn函數const effectFn = () => {// 調用cleanup函數完成清除cleanup(effectFn);// 將定義的effectFn函數設置為當前激活的副作用函數activeEffect = effectFn;fn();};// deps屬性中用來存儲所有與該副作用函數相關聯的依賴集合effectFn.deps = [];effectFn();
}// 清除依賴
function cleanup(effectFn) {// 遍歷effectFn.deps數組const length = effectFn.deps.length;for (let i = 0; i < length; i++) {// 取出依賴集合const deps = effectFn.deps[i];// 將副作用函數從依賴集合中移除deps.delete(effectFn);}// 將effectFn.deps重置為空數組effectFn.deps.length = 0;
}// 測試部分
// 執行副作用函數, 觸發讀取
effect(function effectFn() {console.log("effect is running");document.body.innerText = obj.flag ? obj.text : "not fount";
});// 1秒后對obj.name屬性進行修改
setTimeout(() => {obj.flag = false;obj.text = "測試分支切換";
}, 1000);

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/23459.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/23459.shtml
英文地址,請注明出處:http://en.pswp.cn/web/23459.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

每天寫java到期末考試(6.6)-java文件輸入輸出流實驗

1、用字節流讀寫二進制文件 要求:用DataOutputStreamFileOutputStream類將1,2,…,100,這100個數字寫入到文件 d:\out1.bin里,然后再用DatalnputStreamFilelnputStream類將d:\out1.bin的內讀出來,并輸出到屏幕上。 用DataOutputStreamFileOutputStream寫入二進制數據時,直接調…

單元測試AIR原則:提升代碼質量的秘密武器

文章目錄 引言一、AIR原則1. Automatic&#xff08;自動化&#xff09;2. Independent&#xff08;獨立性&#xff09;3. Repeatable&#xff08;可重復性&#xff09; 二、Automatic&#xff08;自動化&#xff09;三、Independent&#xff08;獨立性&#xff09;四、Repeatab…

【MySQL】sql語句之表操作(上)

序言 在上一篇的數據庫操作的內容中&#xff0c;學習了兩種屬性和常用的七種操作&#xff0c;學習是循序漸進的&#xff0c;庫的操作學完了&#xff0c;就要開始學習表的操作了&#xff0c;而表可與數據強相關&#xff0c;比如DDL&#xff0c;即數據定義語言&#xff0c;DML&am…

DVWA-XSS(Stored)

Low 觀察后端代碼&#xff0c;對輸入進行了一些過濾和轉義。trim(string,charlist) 函數用于移除字符串兩側的空白字符或其他預定義字符&#xff0c;charlist 參數可以規定從字符串中刪除哪些字符。stripslashes() 函數用于刪除反斜杠。mysqli_real_escape_string() 函數用于對…

SAAS系統架構設計剖析

多租戶數據隔離 用戶擔心數據安全性&#xff0c;也就是要做數據隔離&#xff0c;不允許 A 租戶查到 B 租戶的數據 1、軟隔離 數據在一起&#xff0c;只不過帶著租戶 id 查詢 在底層驅動 jar 上進行封裝&#xff0c;強制帶上租戶 id 比如&#xff1a;MySQL、MQ、Redis&#…

【論文精讀】DCRNN-擴散圖卷積循環神經網絡

DCRNN 模型是南加州大學的 Li 等人發表在 I C L R 2018 ICLR 2018 ICLR2018 會議上一個用于交通預測的時空預測模型,論文題目為: 《DIFFUSION CONVOLUTIONAL RECURRENT NEURAL NETWORK: DATA-DRIVEN TRAFFIC FORECASTING》,文章地址為: https://arxiv.org/abs/1707.01926。 …

vs中運行程序時,報不能運行解決方式

問題 在vs中編譯運行程序中&#xff0c;如果程序還在運行&#xff0c;編譯會報錯&#xff0c;但是在后臺又找不到對應的程序 解決方式 1、tasklist | find “進程名” 2、taskkill /PID

【實戰】kafka3.X kraft模式集群搭建

文章目錄 前言kafka2.0與3.x對比準備工作JDK安裝kafka安裝服務器增加hosts 修改Kraft協議配置文件格式化存儲目錄 啟動集群停止集群測試Kafka集群創建topic查看topic列表查看消息詳情生產消息消費消息查看消費者組查看消費者組列表 前言 相信很多同學都用過Kafka2.0吧&#xf…

二叉樹的鏡像--c++【做題記錄】

【問題描述】 給定擴展二叉樹的前序序列&#xff0c;構建二叉樹。 求這課二叉樹的鏡像&#xff0c;并輸出其前序遍歷序列。 【輸入形式】 輸入擴展二叉樹的前序序列。 【輸出形式】 輸出鏡像二叉樹的前序遍歷序列。 【樣例輸入】 ab##cd##e## 【樣例輸出】 鏡像后二叉樹的前序遍…

功能問題:如何防止接口重復請求?

大家好&#xff0c;我是大澈&#xff01; 本文約 1400 字&#xff0c;整篇閱讀約需 3 分鐘。 防止接口重復請求在軟件開發中非常重要&#xff0c;重復請求必然會導致服務器資源的浪費。 因為每次請求都需要服務器進行處理&#xff0c;如果請求是重復的&#xff0c;那么服務…

乘船過河(ship)

合肥市第33屆信息學競賽&#xff08;2016年&#xff09; 題目描述 Description 卡卡西和小朋友們要乘船過河了&#xff0c;港口有很多條船可以租到&#xff0c;并且之間沒有區別&#xff0c;每條船的出租費用也是一樣的。但是一條船最多只能乘坐兩個人&#xff0c;且乘客的總…

STM32 IIC 使用 HAL 庫操作eeprom

在STM32上通過I2C接口&#xff08;注意&#xff1a;在標準STM32庫中&#xff0c;I2C接口通常被寫為"I2C"而不是"IIC"&#xff09;與EEPROM芯片通信時&#xff0c;你需要遵循I2C通信協議&#xff0c;并使用STM32的HAL庫或標準外設庫&#xff08;如果適用&am…

tomcat配置請求的最大參數個數和請求數據大小

maxParameterCount"10000" maxPostSize"10485760" maxParameterCount&#xff1a;單個請求最大請求參數個數&#xff1b; maxPostSize&#xff1a;單個請求最大數據大小&#xff0c;1048576010M&#xff1b;

基本算法——位運算

a^b 原題鏈接&#xff1a;登錄—專業IT筆試面試備考平臺_牛客網 題目描述 運行代碼 #include<iostream> using namespace std; long long a,b,c,t1; int main() {cin>>a>>b>>c;for(;b;b/2){if(b&1)tt*a%c;aa*a%c;}cout<<t%c; } 代碼思路…

汽車軟件 OTA技術解析

汽車軟件 OTA 技術概述 在當今汽車行業中,軟件定義汽車的概念逐漸深入人心。隨著汽車智能化和網聯化的發展,汽車軟件的重要性日益凸顯。而汽車軟件 OTA(Over-the-Air)技術作為一種重要的軟件升級和維護方式,正逐漸成為汽車行業的熱點話題。 汽車軟件 OTA 技術是指通過無線…

邏輯回歸及python實現

概述 logistic回歸是一種廣義線性回歸&#xff08;generalized linear model&#xff09;&#xff0c;因此與多重線性回歸分析有很多相同之處。它們的模型形式基本上相同&#xff0c;都具有 w‘xb&#xff0c;其中w和b是待求參數&#xff0c;其區別在于他們的因變量不同&#x…

App Inventor 2 復制屏幕功能,界面設計更便捷,避免誤刪組件

“復制屏幕”功能全新上線&#xff0c;中文網獨有&#xff08;MIT沒有此功能&#xff09;&#xff0c;可以復制屏幕中的普通組件、圖片、附件、拓展、代碼塊。更多升級詳情可查看發布日志。 下面演示一下屏幕的復制效果&#xff1a; 1、Screen1屏幕上有若干組件、及一個SQLit…

美業SaaS系統源碼分享-收銀管理的主要功能

美業SaaS系統 連鎖多門店美業收銀系統源碼 多門店管理 / 會員管理 / 預約管理 / 排班管理 / 商品管理 / 活動促銷 PC管理后臺、手機APP、iPad APP、微信小程序 ? 博弈美業-收銀管理功能 1、同時支持支付寶和微信支付&#xff0c;具有簡單便捷安全等優點&#xff0c;并且符…

MySQL之查詢性能優化(八)

查詢性能優化 MySQL查詢優化器的局限性 MySQL的萬能"嵌套循環"并不是對每種查詢都是最優的。不過還好&#xff0c;MySQL查詢優化器只對少部分查詢不適用&#xff0c;而且我們往往可以通過改寫查詢讓MySQL高效地完成工作。還有一個好消息&#xff0c;MySQL5.6版本正…

Java開發注意事項

注意&#xff1a;測試類中使用Autowired注解注入Bean&#xff0c;不要使用RequiredArgsConstructor注解注入Bean 正確示范: import org.springframework.boot.test.context.SpringBootTest; import org.springframework.beans.factory.annotation.Autowired;SpringBootTest c…