
這里要給同學們分享的是 Proxy 與雙向系結,我們對大部分的 JavaScript 的這種基礎庫其實已經在其他文章中做過一些講解了,或者是在我們編程的時候有所接觸了,唯有這個 Proxy 我們之前是非常的回避的,因為在業務中也不太推薦大量的使用 Proxy,
Proxy 的設計其實是一種,強大且危險的一種設計,因為應用了 Proxy 的一些代碼,它的 “預期性” 會變差,所以 proxy 這個特性是專門為底層庫而設計的,
Proxy 基本用法
這里我們就一起學習一下 proxy 的基本用法,在后面我們會一起實作一下 Vue 3.0 的 reactive 的模型,當然這里實作的 reactive 并不是一個生產可用的代碼,只是寫一個概念版或者是玩具版的一個 reactive,主要還是用它去認識和學習一下 proxy 有哪些強大的用途,
這里我們邊寫代碼邊了解 Proxy 的一個整體特性,首先我們先創建一個 object,然后我們給這個 object 一些屬性,
let object = {
a: 1,
b: 2
}
現在如果我們去訪問這個 object 的 a 屬性和 b 屬性,這個中間其實是有一個獲取程序,但是在 JavaScript 的底層是一個寫死的方法,也就是說我們無法去干預或者監聽這個獲取物件屬性的程序的代碼,
那么這個 object 它就是一個不可 observe (觀察) 的物件,所以就是一個單純的資料存盤,這也是 JavaScript 最底層的機制,我們是沒有辦法去改變的,
那么如果我們想有一個物件,我們既想它擁有普通物件一樣的特性,又想讓它能夠被監聽,那么我們可以怎么做呢?這個時候我們就可以通過一個
proxy來給object做一層包裹,
那么接下來我們就用 proxy 來實作一個這樣的物件,
- 首先我們需要創建一個
Proxy() - 并且第一個引數需要把我們的
object傳進去 - 然后第二個引數是一個
config的配置物件 - 這個
config物件里面就包含了所有的我們針對proxy物件的鉤子 - 這里我們就做一個最簡單的鉤子
set—— 當我們去設定物件的一個屬性的時候就會觸發我們的set函式 - 這個
set函式會接收我們當前物件、屬性名、屬性值等三個引數
let object = {
a: 1,
b: 2
}
let po = new Proxy(object, {
set(obj, prop, val) {
console.log(obj, prop, val)
}
})
這個時候我們把這個代碼在瀏覽器運行一下,這里我們運行一個 po.a = 6,

這里同學們可以看到,如果 po 是一個普通物件的話這里應該什么代碼都不會去執行的,除非 a 它本身就是一個 setter,但是在我們撰寫的這個 proxy 物件上,不管我們去設定哪一個屬性,都會運行我們的 set 函式,并且獲得不一樣的值,
我們來嘗試設定一個 po 物件中沒有的屬性看看,

首先 proxy 跟 getter 和 setter 最主要的一個區別就是,proxy 物件上即使我們設定一個沒有的屬性,它也會默認觸發這個 set 的方法,
我們的 proxy 里面不只提供了 get、set 這些屬性的鉤子,其實里面還可以攔截并且改變原生的操作或者是對物件進行操作的內置函式的行為,
如果我們上 MDN 的網站上是可以看到所有 proxy 所支持的鉤子,這里列出的有 apply、construct、defineProperty、deleteProperty 等等這一系列的內置或者原生的操作進行攔截并且改變它們的行為,所以說 proxy 物件是一個非常強大的物件,
回到我們的例子中,我們 proxy 實際上就是代理了 object 這個物件,如果我們去呼叫原始的 object上的值,并不會觸發 proxy 上的 hook (鉤子) 里面的函式,
只有使用我們的 po(也就是我們定義的一個 object 物件的 proxy 代理物件) 才會最后去執行到 proxy 物件的攔截行為,而 object 還是原來的 object,
所以我們可以把
po理解成一個特殊的物件,而po上面所有的行為都是可以被重新去指定的,這個也就是為什么我們一開始的時候會說,object 中使用了 proxy 之后物件行為的可預測性就會降低,因為我們看到的一個代碼,比如po.a = 6在執行的時候也許背后就做了一系列很復雜的操作,這些我們是不會知道的,所以 proxy 的這個特性是一個非常危險的特性,
接下來我們來看看 Proxy 的一些應用,
模仿 Reactive 實作原理
這里我們嘗試給物件做一個簡單的包裝, Vue 3.0 其中一個改動就是把 Vue 原來的能力拆了一個包,產生了一個叫reactive 的這一個單獨的包,
Reactive 是一個 Vue 3.0 中非常好的東西,這里我們就嘗試去模仿一下它在 Vue 中的實作原理,如果有看過 Vue 3.0 原始碼的同學應該都會知道,Vue 3.0 中的 reactive 是使用 proxy 來實作的,
那么我們就來一起實作一個玩具版的 reactive 的小練習,從而我們更能了解 proxy 的實際應用場景,
首先我們要知道,一般對 proxy 的使用,都是會對物件做某種監聽或者是改變他行為的事情,所以說對 proxy 的封裝是不會像我們這樣,直接用 new Proxy 這樣的方式,我們都會把它包進一個函式里面,跟我們的 Promise 比較類似,
封裝 reative 函式
所以這里我們先來實作一個包裹起來的 reactive 函式:
reactive函式會接收一個object作為引數- 然后我們的
proxy物件就是這個函式的回傳值 - 之前的
Proxy的config中我們寫了set, 這里我們加上一個get方法 - 然后我們就可以把
po改為使用reactive(object)來監聽它所有的屬性相關的操作了
let object = {
a: 1,
b: 2,
};
let po = reactive(object);
function reactive(object) {
return new Proxy(object, {
set(obj, prop, val) {
console.log(obj, prop, val);
},
get(obj, prop) {
console.log(obj, prop);
},
});
}
就是這樣我們就把
new Proxy給包裝起來了,我們可以看到如果我們想去包裝多個object的話就可以繼續去復用這個reactive的代碼,
然后我們來看看在瀏覽器中運行的效果:
這里我們執行以下 po.a = 666

這里我們就可以得到 set 函式中的 console.log 列印出來的內容了,但是這里面其實還有一個問題,如果我們在 console 中列印 object ,就會發現我們的 object 原來它并沒有變化,就是我們執行的 po.a = 666 并沒有在 object 中生效,

所以這里我們需要在 set 函式中把這個執行改變的代碼加上,讓它實際的去操作這個 object 改變的行為,然后我們同時可以把 get 的功能也實作了,
let object = {
a: 1,
b: 2,
};
let po = reactive(object);
function reactive(object) {
return new Proxy(object, {
set(obj, prop, val) {
obj[prop] = val;
console.log(obj, prop, val);
return obj[prop];
},
get(obj, prop) {
console.log(obj, prop);
return obj[prop];
},
});
}
這時候我們執行 po.x = 666,我們就會發現原始被代理的物件 object 上面已經添加了新的屬性 x,同樣我們也是可以去改原來的變數的,如果我們執行 po.b = 777 那么 object 中的屬性也會跟著發生變化,

這里我們就實作了一個 po 對 object 的一個完全的代理,當然如果我們想真正做一個完整的代理我們是需要把 proxy 中所有的 hook 都要考慮清楚,因為有的時候我們去訪問一個物件或者改變一個物件的時候,其實并不是說通過這種表面的 get、set 的屬性的方式去訪問的,
我們還是可以通過一些內置的方法,比如說 defineProperty ,需要對我們的物件發生作用,這個時候我們就需要把所有的 hook 都補全了,
但是我們可以忽略一些 hook 不去處理,比如說 apply 和 construct,因為它們管的是用 new 去呼叫這個物件和物件后面加圓括號產生的結果,
學習到這里我們已經獲得了一個基本的,能夠代理 object 行為并且可以去監聽 object ,并且包含了所有設定屬性或者改變屬性的行為的一個 proxy 物件,
接下來我們一起來嘗試給他再加入真正的 reactive 特性,讓事件可以變得可監聽,
實作事件監聽
我們有了 reactive 這樣一個函式之后,我們可以考慮一下如何去監聽,當然我們可以給 po 上面去加 addEventListener 類似的操作,但是在 Vue 當中他們用了一個特別有意思的 API,
就是我們可以直接通過 effect 傳一個函式進入來監聽 po 上面的一個屬性,以此來代替這個事件監聽的機制,那么下面我們來嘗試實作一個 “粗糙版”,
- 因為這個
effect是接收一個回呼函式的,所以我們這里需要再寫一個effect函式 - 然后我們的
effect函式需要接收一個callback引數 - 我們需要一個全域的
callbacks陣列變數來儲存我們所有的 callback 函式 - 在
effect函式中我們把傳入進來的 callback 函式給 push 到我們的 callback 陣列中儲存起來 - 這樣的話,我們在
set的時候就直接遍歷callbacks并執行里面所有的回呼函式即可
// 回呼函式儲存組數
let callbacks = [];
let object = {
a: 1,
b: 2,
};
let po = reactive(object);
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
callbacks.push(callback);
}
// 加入一個監聽事件
effect(() => {
console.log('effected a : ', po.a);
});
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
return new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
// 呼叫所有監聽回呼函式
for (let callback of callbacks) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
console.log(obj, prop);
return obj[prop];
},
});
}
這個就是一個非常粗糙的實作了 reactive 中屬性的監聽事件,接下來我們來看看實際效果如何:

這里可以看到我們加入的 effect() 回呼函式確實被執行了,如果我們只考慮實作的正確性,而不考慮性能的話我們就已經完成了 reactive 的操作,但是這個里面顯然它有一個嚴重的性能問題的,比如說我們有 100 個物件,并且給 100 個物件設定了 100 個 effect,那么每次執行一遍就要調一萬遍,因為每次它都把我們全域變數 callbacks 中記錄的回呼函式都執行一遍,
顯然我們實作的這個 reactive 只是一個中間步驟,它并不是一個最終結果,那么我們接下來就去嘗試解決這個問題,看看能不能做到僅傳一個函式就能讓它只有在對應的變數變化的時候,觸發這個函式的呼叫,
建立 reactive 與 effect 連接
上一部分我們建立了物件屬性的監聽,這里我們給 reactive 物件屬性和 effect 函式之間建立獨立的連接,之前我們的 effect 函式與我們 reactive物件屬性是沒有一對一的關系的,這樣 100 個物件就會系結 100 effect,所以這里就會有一個性能隱患,
也就是說如果我們監聽了 po.a 的話,當我們執行 po.a = 2 的時候,我們的 effect 回呼函式就會被執行,但是如果我們執行的是 po.b = 3時,就不應該執行我們的 effect 函式,因為 po.b 并沒有被監聽,
如果我們想實作這樣的效果,我們就需要一個物件屬性與effect 之間的依賴關系,它們之間有一個一對一的關聯關系,互相回應,
讓我們先來嘗試一下建立一個 userReactivities 來儲存我們的監聽物件屬性,
- 首先我們需要準備一個
usedReactivities的全域變數,來儲存我們需要監聽的物件和物件的屬性 - 接著我們嘗試在
effect里面去呼叫一次這個代理物件的屬性,比如po.a,這樣就觸發了這個屬性的監聽,因為我們呼叫了po.a,也就是一個獲取變數值的動作,所以這里就會呼叫到我們reactive中的get,這里我們把物件和物件屬性都注冊進入usedReactivies這個變數里面 - 然后我們改造一下我們的
effect函式,在這里我們首先需要清除一次我們的usedReactivities,保證每次注冊的時候都是全新的,這樣才會清除掉之前監聽的物件屬性,
// 回呼函式儲存組數
let callbacks = [];
// 使用過的函式屬性
let usedReactivities = [];
let object = {
a: 1,
b: 2,
};
let po = reactive(object);
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
// callbacks.push(callback);
usedReactivities = [];
callback();
console.log(usedReactivities);
}
// 加入一個監聽事件
effect(() => {
console.log('effected a : ', po.a);
});
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
return new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
// 呼叫所有監聽回呼函式
for (let callback of callbacks) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
return obj[prop];
},
});
}

這里我們可以看到,在 effect 被呼叫的時候,我們的物件和物件的屬性都被正確的注入到 usedReactivities 之中,這里我們只是做了一個簡單的物件和物件屬性的存盤,并不能讓我們建立物件屬性與 effect 函式的依賴關系,我們需要另外把所有 callbacks 儲存起來,從而讓他們與我們的物件屬性建立依賴關系,
- 接下來我們可以使用
callbacks這個全域變數來存盤我們的依賴關系,所以這里我們就需要把它改造成一個new Map()來存,因為我們需要把object物件作為一個key,這樣我們才可以用它來找到對應的reactivities(物件屬性的對應 callback 函式), - 然后我們就可以去改造我們的
effect函式,在我們呼叫了callback()之后,我們的usedReactivites中就會擁有我們需要監聽的物件和物件屬性了,接著我們就需要注入我們的物件屬性與 effect 依賴關系到callbacks里面, - 我們的
物件屬性與effect的依賴資料是以物件和物件屬性為 key,key[0]是我們的物件,key[1]是我們的物件屬性,我們的 value 就是我們的callback回呼函式 - 有了這個依賴關系,我們就需要在
reactive觸發set的時候根據當前物件和物件屬性找到對應的callback函式來執行,如果找不到就是這個物件屬性沒有被監聽,不需要執行回呼函式,
// 回呼函式儲存組數
let callbacks = new Map();
// 使用過的函式屬性
let usedReactivities = [];
let object = {
a: 1,
b: 2,
};
let po = reactive(object);
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
// callbacks.push(callback);
usedReactivities = [];
callback();
for (let reactivity of usedReactivities) {
if (!callbacks.has(reactivity[0])) {
callbacks.set(reactivity[0], new Map());
}
if (!callbacks.has(reactivity[1])) {
callbacks.get(reactivity[0]).set(reactivity[1], []);
}
callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
}
}
// 加入一個監聽事件
effect(() => {
console.log('effected a : ', po.a);
});
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
return new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
if (callbacks.get(obj))
if (callbacks.get(obj).get(prop))
// 呼叫所有監聽回呼函式
for (let callback of callbacks.get(obj).get(prop)) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
return obj[prop];
},
});
}

最后我們在瀏覽器中運行,我們就會發現執行 po.a=3 觸發了我們 effect 回呼函式,但是 po.b=6 并沒有觸發,這個就是我們想要的效果了,但是我們的代碼還是寫的比較粗糙的,也沒有考慮到解除的效果,不過我們這段代碼已經演示了 reactivity 的實作原理,
優化 reactive
到了這里我們的 effect 和 reactive 已經可以跑起來了,但是其實里面還是有一些小問題的,
比如說現在我們的 object 中的 a 也是一個物件:
let object = {
a: {b: 3},
b: 2
}
然后我們在 effect 里面,呼叫了 po.a.b 這樣的連級物件呼叫,那么這個物件它是一個監聽不到 a 里面的 b 屬性的,
所以說我們有必要對它再進行一些處理,讓它能夠支持 po.a.b 這種形式的呼叫,要滿足這樣的功能,我們就要對 reactive 的 get 和 set 有一定的要求,
當我們 get 中的 obj[prop] 是一個物件的時候,我們就需要給它套一個 reactivity,也就是說當我們檢測到 prop 是一個 object 的話,我們就給它回傳一個 reactive(obj[prop]),
那么我們來改造一下 reactive 中的 get 方法:
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
return new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
if (callbacks.get(obj))
if (callbacks.get(obj).get(prop))
// 呼叫所有監聽回呼函式
for (let callback of callbacks.get(obj).get(prop)) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
if (typeof obj[prop] === 'object') return reactive(obj[prop]);
return obj[prop];
},
});
}
這樣的改造雖然是可以讓我們物件中的物件也被代理了,但是我們會一個問題,就是我們的 reactive 是會回傳一個新的 proxy 的,那就意味著,po.a.b 拿到的 proxy 和 po.a 不是同一個 proxy,
所以這里我們就需要把 proxy 物件放入一個全域的暫存變數里面,方便我們呼叫的時候在快取資料里面重新拿出來,我們就宣告一個 reativities ,默認值為 new Map(),
當每個物件去呼叫 reactivity 的時候,我們會加一個快取,因為 proxy 本身它是不存盤任何狀態的,而所有的狀態都會代理到 object 上,某種意義上講 reactive 其實是一個無狀態的函式,所以我們可以對它進行快取,
我們就在 reactive 的函式開始的位置,加入一個判斷,如果我們快取變數 reactivies 中有這個 object,我們就直接回傳,如果沒有我們就執行我們的新 proxy 生成并且把它存入 reactivies ,
好,我們來看看代碼是怎么實作的,
// 這個callbacks是一個依賴收集而已
// 它表示的是,某個object的某個prop,被一些函式使用了,
// 我們把這些函式存在一個array里,通過 callbacks.get(object).get(props),
// 我們就能拿到這些函式
let callbacks = new Map();
// reactivities 只是保存了object和它對應的proxy的k-v關系,
let reactivities = new Map();
// useReactivities是當我們初次呼叫effect(callback)的時候,
// 會先初始化運行一次callback,然后把依賴關系暫存在useReactivities
// 這個陣列里面,暫存的格式是像下面這樣:
/**
[
[物件A,物件A被依賴的某個屬性],
[物件B,物件B被依賴的某個屬性],
[物件C,物件C被依賴的某個屬性],
...
]
**/
let usedReactivities = [];
let object = {
a: { b: 3 },
b: 2,
};
let po = reactive(object);
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
// callbacks.push(callback);
usedReactivities = [];
callback();
for (let reactivity of usedReactivities) {
if (!callbacks.has(reactivity[0])) {
callbacks.set(reactivity[0], new Map());
}
if (!callbacks.has(reactivity[1])) {
callbacks.get(reactivity[0]).set(reactivity[1], []);
}
console.log('123', callbacks);
callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
}
}
// 加入一個監聽事件
effect(() => {
console.log('effected a.b : ', po.a.b);
});
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
if (reactivities.has(object)) return reactivities.get(object);
let proxy = new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
if (callbacks.get(obj))
if (callbacks.get(obj).get(prop))
// 呼叫所有監聽回呼函式
for (let callback of callbacks.get(obj).get(prop)) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
if (typeof obj[prop] === 'object') return reactive(obj[prop]);
return obj[prop];
},
});
reactivities.set(object, proxy);
return proxy;
}
這樣我們就完成了 reactive 的邏輯,我們來看看實際效果是否正確,

這里我們可以看到,無論是我們直接去改變級聯物件中的 b ,還是給 a 重新賦值一個 {b: 66} 都是可以觸發我們的 effect 回呼函式的,
這就意味著我們最后的 function 已經能夠成功地呼叫和執行了,到這里為止我們的 proxy 和 reactive 的實作和基本的模型就已經有了,當然還有很多細節,是需要我們用大量的 test case 去保證一些邊緣的情況的,如果大家想看看一個真正完成的 reactivity 這個庫是怎么寫的,那么我們可以參考 Vue 的源代碼,大家就可以看到這個代碼量是我們這個的好幾倍不止,
所以講原理和實際操作還是有比較大的區別的,希望大家在學習的時候都理解這一點,要不然我們直接把這些代碼拿過去生產使用,那就要出問題了,
Reactive 回應式物件
接下來我們來考慮一下 Reactive 到底有什么應用場景,這個也是很多同學在 Vue 3.0 出來了以后,在瘋狂的問的一個問題,
其實
Reactive它是一個半成品的雙向系結,它可以負責從資料到 DOM 元素這一條線的監聽,從 DOM 元素到資料的這一條線的監聽其實很簡單,因為 DOM 元素本來就有事件,然后任何的原生輸入都可以代理到這個reactive的代理里面,
我們接下來就考慮一個實際的例子,這里我們來做一個輸入的單向系結來看看,
- 我們給我們之前實作的
reactive中加入一個input元素 - 我們給這個
input綁上一個id="r" - 改造一下我們的
object資料結構 - 在我們的
effect當中加入單向系結(從資料到 input)
<input type="text" id="r" />
<script>
let callbacks = new Map();
let reactivities = new Map();
let usedReactivities = [];
let object = {
r: 1,
};
let po = reactive(object);
// 加入一個監聽事件
effect(() => {
// 加入了單向資料系結
document.getElementById('r').value = po.r;
});
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
usedReactivities = [];
callback();
for (let reactivity of usedReactivities) {
if (!callbacks.has(reactivity[0])) {
callbacks.set(reactivity[0], new Map());
}
if (!callbacks.has(reactivity[1])) {
callbacks.get(reactivity[0]).set(reactivity[1], []);
}
callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
}
}
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
if (reactivities.has(object)) return reactivities.get(object);
let proxy = new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
if (callbacks.get(obj))
if (callbacks.get(obj).get(prop))
// 呼叫所有監聽回呼函式
for (let callback of callbacks.get(obj).get(prop)) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
if (typeof obj[prop] === 'object') return reactive(obj[prop]);
return obj[prop];
},
});
reactivities.set(object, proxy);
return proxy;
}
</script>
然后我們在瀏覽器運行時,我們會看到 input 中的數字是 1,然后在 console 中輸入 po.r = 10,我們就會發現資料會被同步到我們的 input 中,

就是這樣我們就已經實作了初步的資料系結了,那么如果我們想實作 雙向系結需要怎么做呢?其實也很簡單,我們只需要加入 addEventListener 即可實作雙向系結,
在我們的 effect 呼叫的后面加入下面一行代碼即可:
/** ... 代碼省略 ... **/
// 加入一個監聽事件
effect(() => {
// 加入了單向資料系結
document.getElementById('r').value = po.r;
});
// 資料雙向系結 (從 input 到資料)
document.getElementById('r').addEventListener('input', event => po.r = event.target.value);
/** ... 代碼省略 ... **/
這個時候我們回到我們的瀏覽器,在 input 中嘗試輸入數字,我們就會發現我們的 po.r 的值也會回應到值的變化,
接下來我們嘗試實作一個 “回應的顏色選擇器”:
- 我們之前已經有一個屬性
r,現在我們在object里面補上b和g, - 也同時加上這兩個單獨的 input 元素,分別的 id 是
id="b"和id="g" - 然后我們再多加兩個
effect各自給到b和g的 input 元素的, - 給 input 元素中加入
type="range",并且給他們都加上最大與最小值 - 資料雙系結的代碼也要給
b和g加上 - 建立一個
div元素,加上屬性id="color",style="width:100px; height: 100px" - 最后我們需要加入一個全域的 effect,這里面需要在任何 input 輸入變動的時候,回應改變我們 div 盒子的背景顏色
我們來把這些邏輯寫成代碼:
<input id="r" type="range" min="0" max="255" />
<input id="g" type="range" min="0" max="255" />
<input id="b" type="range" min="0" max="255" />
<div id="color" style="width: 100px; height: 100px"></div>
<script>
let callbacks = new Map();
let reactivities = new Map();
let usedReactivities = [];
let object = {
r: 1,
g: 1,
b: 1,
};
let po = reactive(object);
/**
* effect 函式
* @param {Function} callback 回呼函式
* @return void
*/
function effect(callback) {
// callbacks.push(callback);
usedReactivities = [];
callback();
for (let reactivity of usedReactivities) {
if (!callbacks.has(reactivity[0])) {
callbacks.set(reactivity[0], new Map());
}
if (!callbacks.has(reactivity[1])) {
callbacks.get(reactivity[0]).set(reactivity[1], []);
}
callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
}
}
// 紅色顏色輸入值變動
effect(() => {
document.getElementById('r').value = po.r;
});
// 綠色顏色輸入值變動
effect(() => {
document.getElementById('g').value = po.g;
});
// 藍色顏色輸入值變動
effect(() => {
document.getElementById('b').value = po.b;
});
// 資料雙向系結 (從 input 到資料)
// 系結紅色輸入變動
document.getElementById('r').addEventListener('input', event => (po.r = event.target.value));
// 系結綠色輸入變動
document.getElementById('g').addEventListener('input', event => (po.g = event.target.value));
// 系結藍色輸入變動
document.getElementById('b').addEventListener('input', event => (po.b = event.target.value));
// 回應盒子背景顏色
effect(() => {
document.getElementById('color').style.backgroundColor = `rgb(${po.r}, ${po.g}, ${po.b})`;
});
/**
* reactive 相應函式
* @param {Object} object
* @return Object
*/
function reactive(object) {
if (reactivities.has(object)) return reactivities.get(object);
let proxy = new Proxy(object, {
// 物件賦值
set(obj, prop, val) {
obj[prop] = val;
if (callbacks.get(obj))
if (callbacks.get(obj).get(prop))
// 呼叫所有監聽回呼函式
for (let callback of callbacks.get(obj).get(prop)) {
callback();
}
return obj[prop];
},
// 物件取值
get(obj, prop) {
usedReactivities.push([obj, prop]);
if (typeof obj[prop] === 'object') return reactive(obj[prop]);
return obj[prop];
},
});
reactivities.set(object, proxy);
return proxy;
}
</script>

在瀏覽器中運行,我們就可以看到上方有三個滑塊,通過拖動改變每一個滑塊的值,下面的盒子的背景顏色就會根據 r、g、b 三個值而變化,
我們這里所寫的代碼,僅僅是對它的變數和值進行了一下簡單的系結關系,
如果說我們再配合一定的語法糖,比如說我們的 build compiler 那么我們就完全可以把它變成一個零代碼的雙向系結模式,
這也正是雙向系結一個強大之處,在很多時候我們互動不需要使用代碼,即可實作互動,其實想想以前我們用 Jquery 做很多的互動邏輯代碼,我們需要寫很多的邏輯和 update 代碼來實作一個互動的程序,而有了雙向系結后,我們可以花更多時間精力專注于撰寫 Vue 和輸入的關系,
這一切都是基于我們擁有了 reactivity 這種回應式物件,那么 Vue 的 reactivity 包被拆出來之后,也會給大家帶來更有意思的想法和實踐,

博主開始在B站直播學習,歡迎過來《直播間》一起學習,
我們在這里互相監督,互相鼓勵,互相努力走上人生學習之路,讓學習改變我們生活!
學習的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來多一點陪伴,多一點鼓勵,我們一起加油吧! (? ?????)?
我是來自《技術銀河》的三鉆,一位正在重塑知識的技術人,下期再見,
推薦專欄
小伙伴們可以查看或者訂閱相關的專欄,從而集中閱讀相關知識的文章哦,
-
📖 《前端進階》 — 這里包含的文章學習內容需要我們擁有 1-2 年前端開發經驗后,選擇讓自己升級到高級前端工程師的學習內容(這里學習的內容是對應阿里 P6 級別的內容),
-
📖 《資料結構與演算法》 — 到了如今,如果想成為一個高級開發工程師或者進入大廠,不論崗位是前端、后端還是AI,演算法都是重中之重,也無論我們需要進入的公司的崗位是否最后是做演算法工程師,前提面試就需要考演算法,
-
📖 《FCC前端集訓營》 — 根據FreeCodeCamp的學習課程,一起深入淺出學習前端,穩固前端知識,一起在FreeCodeCamp獲得證書
-
📖 《前端星球》 — 以實戰為線索,深入淺出前端多維度的知識點,內含有多方面的前端知識文章,帶領不懂前端的童鞋一起學習前端,在前端開發路上童鞋一起燃起心中那團火🔥

CSDN認證博客專家
前端
Vue
React
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/233136.html
標籤:其他
