你想知道 vue3 中響應式的成長曆程嗎?
vue 的響應性
當我第一次使用 vue
進行專案開發時,響應式資料渲染 是讓我感到最驚奇的一個功能,我們來看下面這段程式碼:
<body>
<div id="app">
<div>
修改商品的數量: <input type="number" v-model="product.quantity">
</div>
<div>
修改商品的價格: <input type= "number" v-model="product.price">
</div>
<p>
總價格:{{ total }}
</p>
</div>
</body>
<script src="https://unpkg.com/[email protected]"></script>
<script>
const component = {
data() {
return {
// 定義一個商品物件,包含價格和數量
product: {
price: 10,
quantity: 2
},
}
},
computed: {
// 計算總價格
total() {
return this.product.price * this.product.quantity
}
}
}
const app = Vue.createApp(component)
app.mount('#app')
</script>
這是一段標準的 vue3
的程式碼,當你在輸入框中輸入內容的時候,totla
這樣的功能我們稱它為 響應式。
響應式的資料渲染 是現在前端非常重要的機制。但是這種機制它究竟是 怎麼被一步一步的構建出來的呢? 這就是這篇部落格想要說的內容。
如果你想要了解 vue3
的 響應系統 及 構建歷程 ,那麼你就應該看下去。
js 的程式性
想要了解 響應性,那麼你需要先了解 程式性。我們來看下面這段普通的 js
程式碼:
// 定義一個商品物件,包含價格和數量
let product = {
price: 10,
quantity: 2
}
// 總價格
let total = product.price * product.quantity;
// 第一次列印
console.log(`總價格:${total}`);
// 修改了商品的數量
product.quantity = 5;
// 第二次列印
console.log(`總價格:${total}`);
想一下,上面的程式碼第一次應該列印什麼內容?第二次應該列印什麼內容?
恭喜你!答對了,因為它們只是普通的 js
程式碼,所以兩次的列印結果應該都是:
總價格:20
但是你有沒有想過,當我們去進行第二次列印的時候,你真的希望它還是 20 嗎?
你有沒有過冒出來這麼一個想法:商品數量發生變化了,如果總價格能夠自己跟隨變化,那就太好了! 這是 人性,從 人 的角度考慮,確實應該這個樣子。但是 程式 並不會如此 ”智慧“。 那麼怎麼能夠讓程式變得更加 “聰明” 呢?
這個 讓程式變 ”聰明“ 的過程,就是響應式構建的過程。
你希望:當資料變化時,重新執行運算
你為了讓你的程式變得更加 “聰明” , 所以你開始想:”如果資料變化了,重新執行運算就好了“。
想到就去做,為了達到這個目的,你開始對運算函式進行了封裝。
你定義了一個匿名函式 effect
,用來計算 商品總價格。並且 當列印總價格前,讓 effect
執行 。所以你得到了下面的程式碼:
// 定義一個商品物件,包含價格和數量
let product = {
price: 10,
quantity: 2
}
// 總價格
let total = 0;
// 計算總價格的匿名函式
let effect = () => {
total = product.price * product.quantity;
};
// 第一次列印
effect();
console.log(`總價格:${total}`); // 總價格:20
// 修改了商品的數量
product.quantity = 5;
// 第二次列印
effect();
console.log(`總價格:${total}`); // 總價格:50
在這樣的一個程式碼中,你得到了一個想要的結果:資料變化了,運算也重新執行了
但是,你很快發現了一個新的問題:這樣的程式碼只能維護單一的總價格運算 。 你希望讓它可以支援更多的運算,那怎麼辦呢?
你希望:當資料變化時,重新執行多個運算
你的程式碼只支援單一運算,你希望讓它支援更多。
為了達到這個目的,你開始對程式碼進行了簡單的封裝,你做了以下三件事情:
- 建立
Set
陣列(點選瞭解 Set) ,用來存放多個運算函式 - 建立
track
函式,用來向Set
中存放運算函式 - 建立
trigger
函式,用來執行所有的運算函式
這樣,你得到了下面的程式碼,並且把這樣的一套程式碼稱之為 響應式:
// -------------建立響應式-------------
// set 陣列,用作儲存所有的運算函式
let deps = new Set();
// 儲存運算函式
function track() {
deps.add(effect);
}
// 觸發器,執行所有的運算函式
function trigger() {
deps.forEach((effect) => effect());
}
// -------------建立資料來源-------------
// 宣告商品物件,為資料來源
let product = {
price: 10,
quantity: 2
};
// 宣告總價格
let total = 0;
// 運算總價格的匿名函式
let effect = () => {
total = product.price * product.quantity;
};
// -------------執行響應式-------------
// 儲存運算函式
track();
// 運算 總價格
effect();
console.log(`總價格:${total}`); // 總價格:20
// 修改資料來源
product.quantity = 5;
// 資料來源被修改,執行觸發器,重新運算所有的 total
trigger();
console.log(`總價格:${total}`); // 總價格:50
你對你的 創造 非常驕傲,並且開始把它推薦給周邊的朋友進行使用。但是很快,就有人提出了問題:我 希望把響應式作用到物件的具體屬性中 ,而不是 一個屬性改變,全部計算重新執行。
你希望:使每個屬性具備單獨的響應性
響應性繫結物件,導致 一個屬性改變,全部計算重新執行。所以你希望把響應式作用到物件的具體屬性中,只 重新運算該屬性相關的內容
為了實現這個功能,你需要藉助 Map 物件。
Map
以 key:val
的形式儲存資料,你希望以 屬性為 key
,以該屬性相關的運算方法集合為 val
。以此你構建了一個 depsMap
物件,用來達到你的目的:
// -------------建立響應式-------------
// Key:Val 結構的集合
let depsMap = new Map();
// 為每個屬性單獨儲存運算函式,從而讓每個屬性具備自己獨立的響應式
function track(key, eff) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(eff)
}
// 觸發器,執行指定屬性的運算函式
function trigger(key) {
// 獲取指定函式的 dep 陣列
const dep = depsMap.get(key);
// 遍歷 dep,執行指定函式的運算函式
if (dep) {
dep.forEach((eff) => eff());
}
}
// -------------建立資料來源-------------
// 宣告商品物件,為資料來源
let product = {
price: 10,
quantity: 2
};
// 宣告總價格
let total = 0;
// 運算總價格的匿名函式
let effect = () => {
total = product.price * product.quantity;
};
// -------------執行響應式-------------
// 儲存運算函式
track('quantity', effect);
// 運算 總價格
effect();
console.log(`總價格:${total}`); // 總價格:20
// 修改資料來源
product.quantity = 5;
// quantity 被修改,僅僅觸發 quantity 的響應式
trigger('quantity');
console.log(`總價格:${total}`); // 總價格:50
</script>
你的客戶總是非常挑剔的,很快他們丟擲了新的問題:我的程式不可能只有一個物件!你需要讓所有的物件都具備響應式!
你希望:使不同物件的不同屬性具備單獨的響應性
你的響應式需要覆蓋程式中的所有物件,否則你的程式碼將毫無意義!
為了達到這個目的,你需要將 物件、屬性、運算方法 進行分別的快取,現有的 depsMap
已經沒有辦法滿足你了。你需要更加強大的 Map
,讓 每個物件 都有一個 Map
。它就是 WeakMap。
WeakMap 物件是一組鍵/值對的集合。其鍵必須是物件,而值可以是任意的。
藉助 WeakMap
你讓每個物件都擁有了一個 depsMap
:
// -------------建立響應式-------------
// weakMap:key 必須為物件,val 可以為任意值
const targetMap = new WeakMap()
// 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
function track(target, key, eff) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 獲取 depsMap 對應的屬性
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 儲存不同物件,不同屬性的 運算函式
dep.add(eff)
}
// 觸發器,執行指定物件的指定屬性的運算函式
function trigger(target, key) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 獲取指定函式的 dep 陣列
const dep = depsMap.get(key);
// 遍歷 dep,執行指定函式的運算函式
if (dep) {
dep.forEach((eff) => eff());
}
}
// -------------建立資料來源-------------
// 宣告商品物件,為資料來源
let product = {
price: 10,
quantity: 2
};
// 宣告總價格
let total = 0;
// 運算總價格的匿名函式
let effect = () => {
total = product.price * product.quantity;
};
// -------------執行響應式-------------
// 儲存運算函式
track(product, 'quantity', effect);
// 運算 總價格
effect();
console.log(`總價格:${total}`); // 總價格:20
// 修改資料來源
product.quantity = 5;
// quantity 被修改,僅僅觸發 quantity 的響應式
trigger(product, 'quantity');
console.log(`總價格:${total}`); // 總價格:50
每次資料改變,我都需要重新執行 trigger
, 這樣太麻煩了!萬一我忘了怎麼辦? 。客戶總是會提出一些 改(wu)進(li)
的要求,沒辦法,誰讓人家是客戶呢?
你希望:使不同物件的不同屬性具備自動的響應性
每次資料改變,我都需要重新執行
trigger
,你的客戶發出了這樣的抱怨。
如果想要達到這樣的目的,那麼你需要了解 “資料的行為” , 即:你需要知道,資料在什麼時候被賦值,在什麼時候被輸出。
此時你需要藉助兩個新的物件:
藉助 Proxy + Reflect
你成功實現了對資料的監聽:
// -------------建立響應式-------------
// weakMap:key 必須為物件,val 可以為任意值
const targetMap = new WeakMap()
// 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
function track(target, key, eff) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 獲取 depsMap 對應的屬性
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 儲存不同物件,不同屬性的 運算函式
dep.add(eff)
}
// 觸發器,執行指定物件的指定屬性的運算函式
function trigger(target, key) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 獲取指定函式的 dep 陣列
const dep = depsMap.get(key);
// 遍歷 dep,執行指定函式的運算函式
if (dep) {
dep.forEach((eff) => eff());
}
}
// 使用 proxy 代理資料來源,以達到監聽的目的
function reactive(target) {
const handlers = {
get(target, key, receiver) {
track(target, key, effect)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (result && oldValue != value) {
trigger(target, key)
}
return result
},
}
return new Proxy(target, handlers)
}
// -------------建立資料來源-------------
// 宣告商品物件,為資料來源
let product = reactive({ price: 10, quantity: 2 })
// 宣告總價格
let total = 0;
// 運算總價格的匿名函式
let effect = () => {
total = product.price * product.quantity;
};
// -------------執行響應式-------------
effect()
console.log(`總價格:${total}`); // 總價格:20
// 修改資料來源
product.quantity = 5;
console.log(`總價格:${total}`); // 總價格:50
你心滿意足,覺得你的程式碼無懈可擊。突然耳邊響起客戶 賞(bu)心(he)悅(shi)目(yi)
的聲音:你不覺得每次執行 effect
很反人類嗎?
你希望:讓運算自動執行
自動化!自動化!所有的操作都應該自動執行!
為了可以讓運算自動執行,你專門設計了一個 effect
函式,它可以 接收運算函式,並自動執行
// -------------建立響應式-------------
// weakMap:key 必須為物件,val 可以為任意值
const targetMap = new WeakMap()
// 運算函式的物件
let activeEffect = null;
// 為不同物件的每個屬性單獨儲存運算函式,從而讓不同物件的每個屬性具備自己獨立的響應式
function track(target, key) {
if (activeEffect) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 獲取 depsMap 對應的屬性
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 儲存不同物件,不同屬性的 運算函式
dep.add(activeEffect)
}
}
// 觸發器,執行指定物件的指定屬性的運算函式
function trigger(target, key) {
// 獲取物件所對應的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 獲取指定函式的 dep 陣列
const dep = depsMap.get(key);
// 遍歷 dep,執行指定函式的運算函式
if (dep) {
dep.forEach((eff) => eff());
}
}
// 使用 proxy 代理資料來源,以達到監聽的目的
function reactive(target) {
const handlers = {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (result && oldValue != value) {
trigger(target, key)
}
return result
},
}
return new Proxy(target, handlers)
}
// 接收運算函式,執行運算函式
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
// -------------建立資料來源-------------
// 宣告商品物件,為資料來源
let product = reactive({ price: 10, quantity: 2 })
// 宣告總價格
let total = 0;
// 通過 effect 運算總價格
effect(() => {
total = product.price * product.quantity;
})
// -------------執行響應式-------------
console.log(`總價格:${total}`); // 總價格:20
// 修改資料來源
product.quantity = 5;
console.log(`總價格:${total}`); // 總價格:50
總結
vue 的響應性讓人驚奇,我們希望瞭解它,更希望知道它的發展歷程。
我們從 JS 的程式性 開始,站在 人性 開始思考,程式應該是什麼樣子的?
我們經歷了 6 個大的階段,最終得到了我們想要的 響應式 系統,而這個也正是 vue3
的響應式在構建時,所經歷的 ”過程“。