手寫 Vue3 數據雙向綁定 理解Proxy
前言
vue3的 Proxy 最近貌似各大網紅公眾號都有發,我也來蹭蹭熱度寫一篇吧!我們也可以結合vue2來看看vue3到底發生了些什么變化。
目錄結構
- Proxy是什么?
- 簡單用法
- 嘗試案例
- proxy - target 參數
- Proxy - handler 參數
- handler
- get()
- set()
- handler
- 什么叫做數據雙向綁定?
- 簡單實現數據渲染
- Proxy實現雙向綁定
- 回顧 Vue2 雙向綁定實現
- Proxy 解決了Vue2的哪些痛點
- Proxy 的缺陷
- 延伸閱讀
Proxy是什么?
Proxy
翻譯過來就是代理的意思,何為代理呢?就是 用 new
創建一個目標對象(traget
)的虛擬化對象,然后代理之后就可以攔截JavaScript
引擎內部的底層對象操作;這些底層操作被攔截后會觸發響應特定操作的陷阱函數。
簡單用法
const p = new Proxy(target, handler)
target
要使用 Proxy
包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
handler
一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p
的行為。
嘗試案例
講再多,看再多,不如寫寫再說
Proxy - target 參數
// 定義一個空對象
let data = {};
// 創建一個 Proxy , 將 data 作為目標對象
let proxy = new Proxy(data, {});
// 修改Proxy 代理對象的name屬性
proxy.name = '嚴老濕';
console.log(proxy); // { name: '嚴老濕' }
console.log(data); // { name: '嚴老濕' }
看了上面的案例,現在的你應該已經大概知道這個 Proxy
的目標對象(target
)是怎么使用的了
Proxy - handler 參數
handler
單獨抽離出來作為一個大標題是因為里面的內容有點多
handler
handler
對象是一個容納一批特定屬性的占位符對象。它包含有 Proxy
的各個捕獲器(trap)。它里面的參數有太多了,我們就拿會用到幾個講講吧!有像深究的同學可以去看看文檔 Proxy handler
[1]
參數 | 作用 |
---|---|
handler.get() | 屬性讀取 操作的捕捉器。 |
handler.set() | 屬性設置 操作的捕捉器。 |
handler.set
handler.set()
方法用於攔截設置屬性值的操作。
文檔上面呢基本上就是這樣寫的
// 定義一個對象
let data = {
name: "嚴老濕",
age: '24'
};
// handler 抽離出來
let handler = {
set(target, prop, value) {
console.log(target, prop, value)
}
}
let p = new Proxy(data, handler);
p.age = 18;
個人習慣直接這樣寫
// 定義一個對象
let data = {
name: "嚴老濕",
age: '24'
};
// 創建一個 Proxy , 將 data 作為目標對象
let p = new Proxy(data, {
set(target, prop, value) {
// target = 目標對象
// prop = 設置的屬性
// value = 修改后的值
console.log(target, prop, value)
// { name: '嚴老濕', age: '24' } 'age' 18
}
});
// 直接修改p就可以了
p.age = 18;
console.log(data)
// { name: '嚴老濕', age: '24' }
在我們設置值的時候會觸發里面的 set
方法;
我們已經捕捉到修改后的 屬性 以及 他的值
但是打印data 並沒有發生任何變化,那這還有啥用呢?
請看官方示例handler.set()
[2]
在示例中出現了一個 Reflect.set()
[3]
return Reflect.set(...arguments);
Reflect
Reflect
對象與Proxy
對象一樣,也是 ES6 為了操作對象而提供的新 API [4]
我們需要在 handler.set()
中 return
一個 Reflect.set(...arguments)
來進行賦值給目標對象。
Reflect.set
Reflect.set
方法設置target
對象的name
屬性等於value
。如果name
屬性設置了賦值函數,則賦值函數的this
綁定receiver
。
Reflect.get
Reflect.get
方法查找並返回target
對象的name
屬性,如果沒有該屬性,則返回undefined
。
let data = {
name: "嚴老濕",
age: '24'
};
let p = new Proxy(data, {
set(target, prop, newV) {
// target = 目標對象
// prop = 設置的屬性
// newV = 修改后的值
return Reflect.set(...arguments)
}
});
p.age = 18;
console.log(data)
// { name: '嚴老濕', age: 18 }
就像這樣,已經打印成功了
handler.get
剛剛我們已經將 set 理解的已經差不多了,get還會難么?我們來看看
let data = {
name: "嚴老濕",
age: '24'
};
let p = new Proxy(data, {
get(target, prop) {
// target = 目標對象
// prop = 獲取的屬性
console.log(target, prop)
// { name: '嚴老濕', age: '24' } 'age'
return Reflect.get(...arguments)
// 這里的 Reflect.get 我們在上面已經講到了
}
});
// 獲取
console.log(p.age)
// 24
什么叫數據雙向綁定?
當數據發生變化的時候,視圖也就發生變化,當視圖發生變化的時候,數據也會跟着同步變化。
上栗子:
html
<h2 class="app"></h2>
js
// 獲取元素
let app = document.querySelector('.app');
// 定義 data
let data = {
name: "嚴老濕",
age: 24
};
// 替換成data.age 此時我們頁面上應該是有個24
app.innerHTML = data.age;
// 我們在這里修改 age
data.age = 21;
console.log(data);
// {name: "嚴老濕", age: 21}
這樣看確實沒啥毛病
但是呢在 vue
中,我們在下面異步修改data中的值,頁面上的值不應該是跟着一起變化的么?雖然data
對象已經發生變化,但是它並不能觸發一些其他操作;
<div id="Foo">
<h2>hello {{msg}}</h2>
<input type="text" v-model="msg">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script> let vm = new Vue({ el: '#Foo', data: { msg: "嚴家輝" } }); </script>
我們現在對雙向綁定有了一個基本的認知。
簡單實現數據渲染
等會兒我們實現雙向綁定,在此之前我們做一個數據渲染過程,也簡單的了解一下其原理
因為內容有點多,所以講解呢全部在注釋里面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./src/index.js"></script>
</head>
<body>
<div id="app">{{name}}
<h2>{{age}}</h2>
</div>
<script> let vm = new Reactive({ // 掛載元素 el:"#app", data:{ name:"嚴老濕", age:24 } }); </script>
</body>
</html>
index.js
class Reactive{
// 接收參數
constructor(options){
this.options = options;
// data 賦值
this.$data = this.options.data;
// 掛載元素
this.el = document.querySelector(this.options.el)
// 調用 compile 函數
this.compile(this.el)
}
// 渲染數據
compile(el){
// 獲取el的子元素
let child = el.childNodes;
// 遍歷判斷是否存在文本
[...child].forEach(node=>{
// 如果node的類型是TEXT_NODE
if(node.nodeType === 3){
// 拿到文本內容
let txt = node.textContent;
// 正則匹配{{}} 空格
let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
if(reg.test(txt)){
let $1 = RegExp.$1;
this.$data[$1] && (node.textContent=txt.replace(reg,this.$data[$1]))
}
// 如果node的類型是ELEMENT_NODE
}else if(node.nodeType === 1){
// 遞歸執行
this.compile(node)
}
})
}
}
圖片來源自 公眾號律神仙ScarSu
一個簡單並且潦草一點的的渲染數據功能已經完成了
Proxy實現雙向綁定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="./src/index.js"></script>
</head>
<body>
<div id="app">{{name}}
<h2>{{age}}</h2>
<input type="text" v-model="name">
{{name}}
</div>
<script> let vm = new Reactive({ // 掛載元素 el: "#app", data: { name: "嚴老濕", age: 24, } }); </script>
</body>
</html>
index.js
// EventTarget [6]
class Reactive extends EventTarget {
// 接收參數
constructor(options) {
super();
this.options = options;
// data 賦值
this.$data = this.options.data;
// 掛載元素
this.el = document.querySelector(this.options.el);
// 調用 compile 函數
this.compile(this.el);
// 調用雙向綁定
this.observe(this.$data);
}
// 雙向綁定
observe(data) {
// 備份this
let _this = this;
// 接收目標對象進行代理
this.$data = new Proxy(data, {
set(target, prop, value) {
// 創建一個自定義事件 CustomEvent [5]
// 事件名稱使用的是 prop
let event = new CustomEvent(prop, {
// 傳入新的值
detail: value
})
// 派發 event 事件
_this.dispatchEvent(event);
return Reflect.set(...arguments);
}
})
}
// 渲染數據
compile(el) {
// 獲取el的子元素
let child = el.childNodes;
// 遍歷判斷是否存在文本
[...child].forEach(node => {
// 如果node的類型是TEXT_NODE
if (node.nodeType === 3) {
// 拿到文本內容
let txt = node.textContent;
// 正則匹配
let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g;
if (reg.test(txt)) {
let $1 = RegExp.$1;
this.$data[$1] && (node.textContent = txt.replace(reg, this.$data[$1]))
// 綁定自定義事件
this.addEventListener($1, e => {
// 替換成傳進來的 detail
node.textContent = txt.replace(reg, e.detail)
})
}
// 如果node的類型是ELEMENT_NODE
} else if (node.nodeType === 1) {
// 獲取attr
let attr = node.attributes;
// 判斷是否存在v-model屬性
if (attr.hasOwnProperty('v-model')) {
// 獲取v-model中綁定的值
let keyName = attr['v-model'].nodeValue;
// 賦值給元素的value
node.value = this.$data[keyName]
// 綁定事件
node.addEventListener('input', e => {
// 當事件觸發的時候我們進行賦值
this.$data[keyName] = node.value
})
}
// 遞歸執行
this.compile(node)
}
})
}
}
這樣我們就實現了一個雙向綁定的小 demo ,當然代碼還不夠嚴謹,比如v-model
的元素篩選都還不夠完善,只是帶大家簡單的了解一下實現邏輯
回顧 Vue2 雙向綁定實現
vue2 大部分同學刷題也經常會碰到 ,我們接下來看看vue2如何實現的呢
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h2 id="txt"></h2>
<input type="text" id="el">
<script> let obj = {}; // 獲取節點 let el = document.querySelector('#el'); let txt = document.querySelector('#txt'); Object.defineProperty(obj, 'foo', { set: function (newValue) { // 修改后的值 進行賦值 txt.innerHTML = newValue; } }); // 綁定事件 el.addEventListener('input', e => { // 賦值給obj數據 obj.foo = e.target.value; }); </script>
</body>
</html>
Proxy解決了vue2的哪些痛點
Object.defineProperty
只能劫持對象的屬性,而Proxy是直接代理對象;Object.defineProperty
對新增屬性需要手動進行Observe
;vue2.x
無法監控到數組下標的變化,因為vue2
放棄了這個特性;- Proxy支持13種攔截操作,這是
defineProperty
所不具有的;
Proxy的缺陷
其他的不想多說,就一個 兼容真的挺難受的,硬傷了
延伸閱讀
[1] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler
[2] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set
[3] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
[4] https://www.cnblogs.com/zczhangcui/p/6486582.html
[5] https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent
[6] https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget