Vue.js
《Vue.js設計與實現》(霍春陽)
- 適合:從零手寫Vue3響應式系統,大廠面試源碼題直接覆蓋。
- 重點章節:第4章(響應式)、第5章(渲染器)、第8章(編譯器)
因為我第一周的任務就是響應式原理(Proxy
vs defineProperty
),手寫簡易reactive
,所以我是從第四章開始學習的。
4.1 響應式數據與副作用函數
1、副作用函數
函數的執行會直接或間接影響其他函數的執行
比如:
01 function effect() {
02 document.body.innerText = 'hello vue3'
03 }
它的執行會使整個body的值都為hello vue3
2、響應式數據
01 const obj = { text: 'hello world' }
02 function effect() {
03 // effect 函數的執行會讀取 obj.text
04 document.body.innerText = obj.text
05 }
如上面的這一段代碼,我們希望當text發生變化的時候,effect會自動執行,這就是所謂的響應式數據
下面我們將會講到這是怎么實現的:
● 當副作用函數 effect 執行時,會觸發字段obj.text 的讀取操作;
● 當修改 obj.text 的值時,會觸發字段 obj.text的設置操作。
實現一個響應式的數據:攔截讀和寫兩個步驟,
讀的時候把effect函數放在一個容器里面
修改的時候就把這個effect函數釋放出來
攔截一個對象的讀取和設置操作:Proxy
const data = { text: 'hello' }; // 定義數據對象const bucket = new Set(); // 設置一個容器用于存儲副作用函數const obj = new Proxy(data, {// 讀的時候攔截get(target, key) {bucket.add(effect); // 將當前活躍的副作用函數添加到容器return target[key]; // 返回屬性值},// 攔截設置操作set(target, key, newVal) {target[key] = newVal;bucket.forEach(fn => fn()); // 執行所有存儲的副作用函數return true;}});const effect = () => {document.body.innerText = obj.text; // 副作用函數依賴于響應式數據};effect();setTimeout(() => {obj.text = "world"; // 修改數據,觸發響應式更新}, 1000); // 延遲1秒修改數據
繼續強化->我們現在硬編碼了effect,但是如果副作用函數的名字不叫effect的話,這段代碼就無法繼續工作了
所以我們要提供一個用來注冊副作用函數的機制
// 用一個全局變量存儲被注冊的副作用函數let activeEffect;function effect (fn) {activeEffect = fn ; fn();}const bucket = new Set(); //設置一個容器const data = { text: 'world' }; // 初始化數據對象const obj = new Proxy(data , {//讀的時候攔截放在容器里面get( tartget, key ){if(activeEffect){bucket.add(activeEffect);}return tartget[key];//返回屬性值},set(target , key , newVal){// 攔截設置操作target[key] = newVal;bucket.forEach(fn => fn());return true;}})effect(() => {console.log('run');document.body.innerText = obj.text;})setTimeout(() => {obj.text = "hello"} , 1000)
當我們為obj添加新的屬性的時候
setTimeout(() => {obj.notExist = "hello vue3"} , 1000)
匿名副作用函數內沒有讀取這個新的屬性的值,那么在1s之后不會起到寫操作(即放出桶里面的所有函數),但是我嘗試了一下,發現它執行了的。
我們為了解決這個問題,就要重新設計“桶”這個數據結構:讓它無論讀取的哪一個屬性都會將副作用函數收到桶里面,設置屬性的時候,無論設置的是哪一個屬性,也都會將副作用函數取出并執行。
let activeEffect;function effect (fn) {activeEffect = fn; fn();}const bucket = new WeakMap();const data = { text: 'world' }; // 確保所有屬性都已定義const obj = new Proxy(data, {get(target, key){if(!activeEffect){return target[key];}// 根據tartget取來的depsMap,它是一個map類型let depsMap = bucket.get(target);// 如果不存在if(!depsMap){// 創建一個bucket.set(target, (depsMap = new Map()));}// 根據key取來的deps,它是一個set類型let deps = depsMap.get(key);// 如果不存在if(!deps){// 創建一個depsMap.set(key, (deps = new Set()));}deps.add(activeEffect); // 添加當前活躍的副作用函數return target[key];},set(target, key, newVal){target[key] = newVal;const depsMap = bucket.get(target);if(!depsMap){return;}const effects = depsMap.get(key);effects && effects.forEach(fn => fn()); // 只觸發與鍵相關的副作用函數}});effect(() => {console.log('run');document.body.innerText = obj.text;});setTimeout(() => {obj.text = "hello vue3"; // 修改已定義的屬性以觸發依賴}, 1000);
大家可以發現,我們引用了weakMap(它與map最大的不同就是它對key是弱引用,不影響垃圾回收器的工作,通常存儲只有當key所引用的對象存在時,才有價值的信息);
我們要解決的是屬性不存在時候的問題,那么
- 在讀的時候判斷是否有這個屬性,沒有就創建一個,有的話就把函數放在桶里面。
- 在修改的時候也是要判斷
最后,我們將一些函數進行封裝:
<script setup>let activeEffect;function effect (fn) {activeEffect = fn; fn();}const bucket = new WeakMap();const data = { text: 'world' }; // 確保所有屬性都已定義const obj = new Proxy(data, {get(target, key){track(target , key);return target[key];},set(target, key, newVal){target[key] = newVal;trigger(target , key , newVal);}});// 追蹤變化function track(target , key){if(!activeEffect){return target[key];}// 根據tartget取來的depsMap,它是一個map類型let depsMap = bucket.get(target);// 如果不存在if(!depsMap){// 創建一個bucket.set(target, (depsMap = new Map()));}// 根據key取來的deps,它是一個set類型let deps = depsMap.get(key);// 如果不存在if(!deps){// 創建一個depsMap.set(key, (deps = new Set()));}deps.add(activeEffect); // 添加當前活躍的副作用函數}// 觸發變化function trigger(target , key , newVal){const depsMap = bucket.get(target);if(!depsMap){return;}const effects = depsMap.get(key);effects && effects.forEach(fn => fn()); // 只觸發與鍵相關的副作用函數}effect(() => {console.log('run');document.body.innerText = obj.text;});setTimeout(() => {obj.text = "hello vue3"; // 修改已定義的屬性以觸發依賴}, 1000);
</script>