淺談 Vue3 雙向綁定的概念與實作

前言

接下來這一篇將會針對 Vue3 來做一下淺談,其中也會包含實作的部分。

Vue

在前一篇 「淺談 Vue2 雙向綁定的概念與實作」有分享了基本 Vue2 的雙向綁定主要來自 Object.defineProperty,但是在 Vue3 之後 Vue 做出了一個非常重要的改變,也就是 Vue3 改使用 ES6 Proxy 用於取代原本的 Object.defineProperty,最常聽到的就是效能有非常大的提升。

使用 Proxy 製作除了效能有非常大的提升之外,最主要是有兩個缺點:

  • 無法深層監聽資料,因此在 Vue 中是採用迴圈方向去往深層監聽,因此效能上就會比較差。
  • 陣列長度若發生變化會失效。

這邊也可以看到 Vue2 的官方文件「检测变化的注意事项」中有這一段話:

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

上面這一段是什麼意思呢?首先先讓來講講一個基本觀念,相信學習 Vue 的人都會聽到一句話「請盡可能預先定義好你的資料,否則會出現問題」,因此如果你的 data 中沒有這個屬性,後來再新增時是無法雙向綁定的:

1
2
3
4
<div id="app">
{{ a }}
{{ b }}
</div>
1
2
3
4
5
6
7
8
var vm = new Vue({
el: '#app',
data:{
a:1
}
})

vm.b = 2

那麼由於無法雙向綁定,因此若綁定到畫面時是會出現 ReferenceError: b is not defined 的錯誤,如果看不出來的話,也可以試著將程式碼改成以下:

1
2
3
4
<div id="app">
{{ a }}
<button @click="getData">點我</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = new Vue({
el: '#app',
data:{
a:1
},
methods: {
getData() {
console.log(this);
}
}
})

vm.b = 2

我們都知道如果使用了 Object.defineProperty 的話會具備有 SetterGetter,但這邊後來新增的 b 屬性並沒有:

無雙向綁定

(若對於 Object.defineProperty 不熟建議閱讀這一篇文章)

因此官方就有提供了一個方法可以解決,也就是 Vue.$set 語法。

那麼接下來聊聊陣列的長度部分,在官方文件中就有舉例到兩點 Vue 無法監聽的陣列狀況:

当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength

通常來講,我們會使用到陣列的時候大多都是為了跑迴圈才使用陣列,而這邊我也查了一下相關文獻,剛好就有一篇 文章 有人提到了這件事情,簡單來講就是,針對陣列去做 Getter 與 Setter 是非常吃效能的,所以通常都必須額外拉出來做效能處理,詳情可以直接看上面文章即可。

這邊你也可以看到實際範例,如果修改了陣列長度或是確實是會失去雙向綁定的:

1
2
3
4
<div id="app">
{{ arr }}
<button @click="len">點我</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var vm = new Vue({
el: '#app',
data:{
arr: [1, 2, 3]
},
methods: {
len() {
this.arr.length = '2'
console.log(this);
},
add() {
this.arr[4] = 4;
console.log(this);
}
}
})

因此你只能使用 Vue 有處理過的語法去操作陣列,否則是會失去雙向綁定:

1
2
3
4
5
6
7
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

ES6 Proxy

接下來聊聊 Proxy,簡單來講他有非常多的操作(狹持、操作)方法(高達 13 種)而且可以代理一整個物件並且返回一個新的物件,以下是它的基本用法

1
let p = new Proxy(target, handler);

剛剛也有講到它會返回一個新的物件因此不會修改到原始物件,而寫法也與原本的 Object.defineProperty 雷同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
myName: 'Ray',
};
var p = new Proxy(obj, {
get(target, name) {
return target[name];
}
})
p.myName; // Ray

var p2 = new Proxy(obj, {
get(target, name) {
return target[name];
}
})

p === p2; // false

Proxy 的參數主要有兩個:

  • target (目標)
    • 可以是任何東西,舉凡物件、陣列或是函式都可以
  • handler (操作)
    • 主要是一個物件,裡面會有許多 target 的操作行為

那麼由於 handler 中的方法太多,所以建議直接考 MDN 文件。

那麼接下來聊聊實作雙向綁定的部分,絕大部分程式碼會直接使用 淺談 Vue2 雙向綁定的概念與實作 這一篇,因此不會重新再說明一次程式碼邏輯,這邊就直接貼上 Object.defineProperty 調整成 new Proxy 的結果:

1
2
3
4
5
<div id="app">
我的名字是:<span v-model="myName"></span>
<br>
<input type="text" v-model="myName">
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var model = {
el: '#app',
data: {
myName: '',
},
};

var data = model.data;
var app = document.querySelector(model.el);
var VModel = app.querySelectorAll('[v-model]');

var p = new Proxy(data, {
get(target, prop, receiver) {
return target[prop];
},
set(target, prop, val) {
var elementKeys = app.querySelectorAll(`[v-model=${prop}]`);
elementKeys.forEach((item) => {
item.value = val; // 針對 input 發生變化時也寫
item.innerText = val; // 渲染到畫面上
})
target[prop] = val;
}
});

VModel.forEach((element) => {
element.addEventListener('input', function() {
p[this.getAttribute('v-model')] = this.value;
})
});

setTimeout(() => {
console.log(p.myName);
}, 5000)

以上就是簡單的淺談與將上一篇 Vue2 改成 Proxy 的方式而已。

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ