JavaScript 實(shí)現(xiàn)一個(gè)響應(yīng)式系統(tǒng)的解決方案
第一階段目標(biāo)
- 數(shù)據(jù)變化重新運(yùn)行依賴數(shù)據(jù)的過程
第一階段問題
- 如何知道數(shù)據(jù)發(fā)生了變化
- 如何知道哪些過程依賴了哪些數(shù)據(jù)
第一階段問題的解決方案
- 我們可用參考現(xiàn)有的響應(yīng)式系統(tǒng)(vue)
vue2 是通過
Object.defineProperty實(shí)現(xiàn)數(shù)據(jù)變化的監(jiān)控,詳細(xì)查看 Vue2官網(wǎng)。vue3 是通過
Proxy實(shí)現(xiàn)數(shù)據(jù)變化的監(jiān)控,詳細(xì)查看 Vue3官網(wǎng)。 - 本次示例使用
Proxy實(shí)現(xiàn)數(shù)據(jù)監(jiān)控,Proxy詳細(xì)信息查看官網(wǎng)。 - 根據(jù)解決方案,需要改變第一階段目標(biāo)為->
Proxy對(duì)象變化重新運(yùn)行依賴數(shù)據(jù)的過程 - 問題變更->如何知道
Proxy發(fā)生了變化 - 問題變更->如何知道哪些函數(shù)依賴了哪些
Proxy
如何知道 Proxy 對(duì)象發(fā)生了變化,示例代碼
//這里傳入一個(gè)對(duì)象,返回一個(gè)Proxy對(duì)象,對(duì)Proxy對(duì)象的屬性的讀取和修改會(huì)觸發(fā)內(nèi)部的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);
},
});
}數(shù)據(jù)監(jiān)控初步完成,但是這里只監(jiān)控了屬性的讀取和設(shè)置,還有很多操作沒有監(jiān)控,以及數(shù)據(jù)的 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) {
//當(dāng)讀取的值是一個(gè)對(duì)象,需要重新代理這個(gè)對(duì)象
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);
},
});
}如何知道哪些函數(shù)依賴了哪些 Proxy 對(duì)象
問題:依賴 Proxy 對(duì)象的函數(shù)要如何收集
在收集依賴 Proxy 對(duì)象的函數(shù)的時(shí)候出現(xiàn)了一個(gè)問題: 無法知道數(shù)據(jù)在什么環(huán)境使用的,拿不到對(duì)應(yīng)的函數(shù)
解決方案
既然是因?yàn)闊o法知道函數(shù)的執(zhí)行環(huán)境導(dǎo)致的無法找到對(duì)應(yīng)函數(shù),那么我們只需要給函數(shù)一個(gè)固定的運(yùn)行環(huán)境就可以知道函數(shù)依賴了哪些數(shù)據(jù)。
示例
//定義一個(gè)變量
export let currentFn;
export function trackFn(fn) {
return function FnTrackEnv() {
currentFn = FnTrackEnv;
fn();
currentFn = null;
};
}自此,我們的函數(shù)調(diào)用期間 Proxy 對(duì)象監(jiān)聽到的數(shù)據(jù)讀取在 currentFn 函數(shù)內(nèi)部發(fā)生的。
同樣,我們的目標(biāo)從最開始的 數(shù)據(jù)變化重新運(yùn)行依賴數(shù)據(jù)的過程 -> Proxy 對(duì)象變化重新運(yùn)行依賴收集完成的函數(shù)
完善函數(shù)調(diào)用環(huán)境
直接給全局變量賦值,在函數(shù)嵌套調(diào)用的情況下,這個(gè)依賴收集,會(huì)出現(xiàn)問題
let obj1 = relyOnCore({ a: 1, b: 2, c: { d: 3 } });
function fn1() {
let a = obj1.a;
function fn2() {
let b = obj1.b;
}
//這里的c會(huì)無法收集依賴
let c = obj1.c;
}我們修改一下函數(shù)收集
export const FnStack = [];
export function trackFn(fn) {
return function FnTrackEnv() {
FnStack.push(FnTrackEnv);
fn();
FnStack.pop(FnTrackEnv);
};
}第二階段目標(biāo)
- 在合適的時(shí)機(jī)觸發(fā)合適的函數(shù)
第二階段問題
- 在什么時(shí)間觸發(fā)函數(shù)
- 到達(dá)觸發(fā)時(shí)間時(shí),應(yīng)該觸發(fā)什么函數(shù)
第一個(gè)問題:在什么時(shí)間觸發(fā)函數(shù)
必然是在修改數(shù)據(jù)完成之后觸發(fā)函數(shù)
第二個(gè)問題:應(yīng)該觸發(fā)什么函數(shù)
當(dāng)操作會(huì)改變函數(shù)讀取的信息的時(shí)候,需要重新運(yùn)行函數(shù)。因此,我們需要建立一個(gè)映射關(guān)系
{
//對(duì)象
"obj": {
//屬性
"key": {
//對(duì)屬性的操作
"handle": ["fn"] //對(duì)應(yīng)的函數(shù)
}
}
}
在數(shù)據(jù)改變的時(shí)候,我們只需要根據(jù)映射關(guān)系,循環(huán)運(yùn)行 handle 內(nèi)的函數(shù)
數(shù)據(jù)讀取和函數(shù)建立聯(lián)系
我們可以創(chuàng)建一個(gè)函數(shù)用于建立這種聯(lián)系
export function track(object, handle, key, fn) {}這個(gè)函數(shù)接收 4 個(gè)參數(shù),object(對(duì)象),handle(對(duì)數(shù)據(jù)的操作類型) key(操作了對(duì)象的什么屬性),fn(需要關(guān)聯(lián)的函數(shù))
我們現(xiàn)在來創(chuàng)建映射關(guān)系
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);
};現(xiàn)在已經(jīng)實(shí)現(xiàn)了數(shù)據(jù)和函數(shù)之間的關(guān)聯(lián)只需要在讀取數(shù)據(jù)時(shí)調(diào)用這個(gè)方法去收集依賴就可以,代碼如下:
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);
},
//....這里省略剩余代碼
});
}接下來我們需要建立數(shù)據(jù)改變->影響哪些數(shù)據(jù)的讀取之間的關(guān)聯(lián)
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]],
]);建立這樣關(guān)聯(lián)后,我們只需要在數(shù)據(jù)變動(dòng)的時(shí)候,根據(jù)映射關(guān)系去尋找需要重新運(yùn)行的函數(shù)就可以實(shí)現(xiàn)響應(yīng)式。
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();
});
}總結(jié)
以上簡(jiǎn)易的實(shí)現(xiàn)了響應(yīng)式系統(tǒng),只是粗略的介紹了如何實(shí)現(xiàn),會(huì)存在一些 bug
到此這篇關(guān)于JavaScript 如何實(shí)現(xiàn)一個(gè)響應(yīng)式系統(tǒng)的文章就介紹到這了,更多相關(guān)JavaScript 響應(yīng)式系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript dragable的Move對(duì)象
一個(gè)dragable的Move對(duì)象,大家可以運(yùn)行下,測(cè)試看下效果。2009-08-08
JavaScript實(shí)現(xiàn)div的鼠標(biāo)拖拽效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)div的鼠標(biāo)拖拽效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
解決input輸入框僅支持輸入數(shù)字及兩位小數(shù)點(diǎn)的限制
這篇文章主要為大家介紹了解決input輸入框僅支持輸入數(shù)字及兩位小數(shù)點(diǎn)的限制技巧示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
webpack-cli在webpack打包中的作用小結(jié)
webpack?是打包代碼時(shí)依賴的核心內(nèi)容,而?webpack-cli?是一個(gè)用來在命令行中運(yùn)行?webpack?的工具,那么webpack-cli在webpack打包中的作用是什么,本文就詳細(xì)的介紹一下,感興趣的可以了解一下2022-04-04
javascript 數(shù)字格式化輸出的實(shí)現(xiàn)代碼
這篇文章主要是對(duì)javascript中數(shù)字格式化輸出的實(shí)現(xiàn)代碼進(jìn)行了介紹,需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-12-12

