JavaScript 實現一個響應式系統的解決方案
第一階段目標
- 數據變化重新運行依賴數據的過程
第一階段問題
- 如何知道數據發(fā)生了變化
- 如何知道哪些過程依賴了哪些數據
第一階段問題的解決方案
- 我們可用參考現有的響應式系統(vue)
vue2 是通過
Object.defineProperty
實現數據變化的監(jiān)控,詳細查看 Vue2官網。vue3 是通過
Proxy
實現數據變化的監(jiān)控,詳細查看 Vue3官網。 - 本次示例使用
Proxy
實現數據監(jiān)控,Proxy
詳細信息查看官網。 - 根據解決方案,需要改變第一階段目標為->
Proxy
對象變化重新運行依賴數據的過程 - 問題變更->如何知道
Proxy
發(fā)生了變化 - 問題變更->如何知道哪些函數依賴了哪些
Proxy
如何知道 Proxy 對象發(fā)生了變化,示例代碼
//這里傳入一個對象,返回一個Proxy對象,對Proxy對象的屬性的讀取和修改會觸發(fā)內部的get,set方法 function relyOnCore(obj) { if (typeof obj !== "object" || obj === null) { return obj; } return new Proxy(obj, { get(target, key, receiver) { return target[key]; }, set(target, key, value, receiver) { //這里需要返回是否修改成功的Boolean值 return Reflect.set(target, key, value); }, }); }
數據監(jiān)控初步完成,但是這里只監(jiān)控了屬性的讀取和設置,還有很多操作沒有監(jiān)控,以及數據的 this 指向,我們需要完善它
//完善后的代碼 export function relyOnCore(obj) { if (typeof obj !== "object" || obj === null) { return obj; } return new Proxy(obj, { get(target, key, receiver) { if (typeof target[key] === "object" && target[key] !== null) { //當讀取的值是一個對象,需要重新代理這個對象 return relyOnCore(target[key]); } return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver); }, ownKeys(target) { return Reflect.ownKeys(target); }, getOwnPropertyDescriptor(target, key) { return Reflect.getOwnPropertyDescriptor(target, key); }, has(target, p) { return Reflect.has(target, p); }, deleteProperty(target, key) { return Reflect.deleteProperty(target, key); }, defineProperty(target, key, attributes) { return Reflect.defineProperty(target, key, attributes); }, }); }
如何知道哪些函數依賴了哪些 Proxy 對象
問題:依賴 Proxy 對象的函數要如何收集
在收集依賴 Proxy 對象的函數的時候出現了一個問題: 無法知道數據在什么環(huán)境使用的,拿不到對應的函數
解決方案
既然是因為無法知道函數的執(zhí)行環(huán)境導致的無法找到對應函數,那么我們只需要給函數一個固定的運行環(huán)境就可以知道函數依賴了哪些數據。
示例
//定義一個變量 export let currentFn; export function trackFn(fn) { return function FnTrackEnv() { currentFn = FnTrackEnv; fn(); currentFn = null; }; }
自此,我們的函數調用期間 Proxy 對象監(jiān)聽到的數據讀取在 currentFn 函數內部發(fā)生的。
同樣,我們的目標從最開始的 數據變化重新運行依賴數據的過程 -> Proxy 對象變化重新運行依賴收集完成的函數
完善函數調用環(huán)境
直接給全局變量賦值,在函數嵌套調用的情況下,這個依賴收集,會出現問題
let obj1 = relyOnCore({ a: 1, b: 2, c: { d: 3 } }); function fn1() { let a = obj1.a; function fn2() { let b = obj1.b; } //這里的c會無法收集依賴 let c = obj1.c; }
我們修改一下函數收集
export const FnStack = []; export function trackFn(fn) { return function FnTrackEnv() { FnStack.push(FnTrackEnv); fn(); FnStack.pop(FnTrackEnv); }; }
第二階段目標
- 在合適的時機觸發(fā)合適的函數
第二階段問題
- 在什么時間觸發(fā)函數
- 到達觸發(fā)時間時,應該觸發(fā)什么函數
第一個問題:在什么時間觸發(fā)函數
必然是在修改數據完成之后觸發(fā)函數
第二個問題:應該觸發(fā)什么函數
當操作會改變函數讀取的信息的時候,需要重新運行函數。因此,我們需要建立一個映射關系
{ //對象 "obj": { //屬性 "key": { //對屬性的操作 "handle": ["fn"] //對應的函數 } } }
在數據改變的時候,我們只需要根據映射關系,循環(huán)運行 handle 內的函數
數據讀取和函數建立聯系
我們可以創(chuàng)建一個函數用于建立這種聯系
export function track(object, handle, key, fn) {}
這個函數接收 4 個參數,object(對象),handle(對數據的操作類型) key(操作了對象的什么屬性),fn(需要關聯的函數)
我們現在來創(chuàng)建映射關系
export const ObjMap = new WeakMap(); export const handleType = { GET: "GET", SET: "SET", Delete: "Delete", Define: "Define", Has: "Has", getOwnPropertyDescriptor: "getOwnPropertyDescriptor", ownKeys: "ownKeys", }; export function track(object, handle, key, fn) { setObjMap(object, key, handle, fn); } function setObjMap(obj, key, handle, fn) { if (!ObjMap.has(obj)) { ObjMap.set(obj, new Map()); } setKeyMap(obj, key, handle, fn); } const setKeyMap = (obj, key, handle, fn) => { let keyMap = ObjMap.get(obj); if (!keyMap.has(key)) { keyMap.set(key, new Map()); } setHandle(obj, key, handle, fn); }; const setHandle = (obj, key, handle, fn) => { let keyMap = ObjMap.get(obj); let handleMap = keyMap.get(key); if (!handleMap.has(handle)) { handleMap.set(handle, new Set()); } setFn(obj, key, handle, fn); }; const setFn = (obj, key, handle, fn) => { let keyMap = ObjMap.get(obj); let handleMap = keyMap.get(key); let fnSet = handleMap.get(handle); fnSet.add(fn); };
現在已經實現了數據和函數之間的關聯只需要在讀取數據時調用這個方法去收集依賴就可以,代碼如下:
export function relyOnCore(obj) { if (typeof obj !== "object" || obj === null) { return obj; } return new Proxy(obj, { get(target, key, receiver) { track(target, handleType.GET, key, FnStack[FnStack.length - 1]); if (typeof target[key] === "object" && target[key] !== null) { return relyOnCore(target[key]); } return Reflect.get(target, key, receiver); }, //....這里省略剩余代碼 }); }
接下來我們需要建立數據改變->影響哪些數據的讀取之間的關聯
export const TriggerToTrackMap = new Map([ [handleType.SET, [handleType.GET, handleType.getOwnPropertyDescriptor]], [ handleType.Delete, [ handleType.GET, handleType.ownKeys, handleType.Has, handleType.getOwnPropertyDescriptor, ], ], [handleType.Define, [handleType.ownKeys, handleType.Has]], ]);
建立這樣關聯后,我們只需要在數據變動的時候,根據映射關系去尋找需要重新運行的函數就可以實現響應式。
export function trigger(object, handle, key) { let keyMap = ObjMap.get(object); if (!keyMap) { return; } let handleMap = keyMap.get(key); if (!handleMap) { return; } let TriggerToTrack = TriggerToTrackMap.get(handle); let fnSet = new Set(); TriggerToTrack.forEach((handle) => { let fnSetChiren = handleMap.get(handle); if (fnSetChiren) { fnSetChiren.forEach((fn) => { if (fn) { fnSet.add(fn); } }); } }); fnSet.forEach((fn) => { fn(); }); }
總結
以上簡易的實現了響應式系統,只是粗略的介紹了如何實現,會存在一些 bug
到此這篇關于JavaScript 如何實現一個響應式系統的文章就介紹到這了,更多相關JavaScript 響應式系統內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!