Vue2?與?Vue3?的數(shù)據(jù)綁定原理及實現(xiàn)
介紹
數(shù)據(jù)綁定是一種把用戶界面元素(控件)的屬性綁定到特定對象上面并使其同步的機制,使開發(fā)人員免于編寫同步視圖模型和視圖的邏輯。
觀察者模式又稱為發(fā)布-訂閱模式,定義對象間的一種一對多的依賴關(guān)系,當(dāng)它本身的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新。比如用戶界面可以作為一個觀察者,業(yè)務(wù)數(shù)據(jù)是被觀察者,用戶界面觀察業(yè)務(wù)數(shù)據(jù)的變化,發(fā)現(xiàn)數(shù)據(jù)變化后,就同步顯示在界面上。這樣可以確保界面和數(shù)據(jù)之間劃清界限,假定應(yīng)用程序的需求發(fā)生變化,需要修改界面的表現(xiàn),只需要重新構(gòu)建一個用戶界面,業(yè)務(wù)數(shù)據(jù)不需要發(fā)生變化。有以下幾個角色:
- 抽象主題(Subject):提供一個接口,把所有觀察者對象的引用保存到一個集合里,可以增加和刪除觀察者對象。
- 具體主題(Concrete Subject):將有關(guān)狀態(tài)信息存入觀察者對象,在本身的內(nèi)部狀態(tài)改變時,給所有登記過的觀察者發(fā)出通知。
- 抽象觀察者(Observer):為所有的具體觀察者定義一個接口,在得到主題通知時更新自己。
- 具體觀察者(Concrete Observer):實現(xiàn)更新接口。
Vue2 和 Vue3 的數(shù)據(jù)綁定都是觀察者模式的實現(xiàn),前者使用 Object.defineProperty,后者使用的是 Proxy。
有以下 HTML:
<div id="app"> <input type="radio" name="hello" id="hello" value="hello" v-model="title"> <label for="hello">hello</label> <input type="radio" name="hello" id="hello2" value="hello2" v-model="title"> <label for="hello2">hello2</label> <div v-bind="title"></div> <input v-model="content"> <select v-model="content"> <option>world</option> <option>world1</option> <option>world2</option> </select> <div v-bind="content"></div> <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby"> <label for="hobby1">hobby1</label> <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby"> <label for="hobby2">hobby2</label> <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby"> <label for="hobby3">hobby3</label> <br> {{ hobby }} </div> <script> const vm = new Vue({ el: '#app', data: { title: 'hello', content: 'world2', hobby: ['hobby2'], } }); </script>
下面使用兩種方法進行簡單實現(xiàn)上面的雙向綁定。
Object.defineProperty
語法:
Object.defineProperty(obj, prop, descriptor)
- obj:要定義屬性的對象。
- prop:要定義或修改的屬性的名稱或 Symbol 。
- descriptor:要定義或修改的屬性描述符。
- 返回值:被傳遞給函數(shù)的對象。
首先定義一個觀察者構(gòu)造函數(shù),并實現(xiàn)得到主題通知時更新自己的邏輯。第一行將當(dāng)前觀察者綁定到函數(shù)屬性上面,是為了避免全局作用域變量。
function Observer(vm, node, name, nodeType) { // 構(gòu)造函數(shù)被調(diào)用時,將當(dāng)前對象綁定到函數(shù)屬性上面,接下來觸發(fā) getter 時使用 Observer.target = this; this.update = () => { // 這里 vm[name] 讀取操作會觸發(fā) getter if (node.type === 'radio') node.checked = node.value === vm[name]; else if (node.type !== 'checkbox') node[nodeType] = vm[name]; }; this.update(); Observer.target = null; // 設(shè)置為空,避免首次觸發(fā)get后重復(fù)添加 }
然后定義 Vue 構(gòu)造函數(shù),遍歷 options.data 對象,為每個屬性都生成一個主題(包含當(dāng)前屬性的觀察者數(shù)組),然后使用 Object.defineProperty 劫持屬性的讀取和寫入操作,在首次讀取時添加一個對應(yīng)的觀察者對象,為了避免后面讀取操作重復(fù)添加,在觀察者構(gòu)造函數(shù)里面首次更新操作完成后設(shè)置了空。
function Vue(options) { const obj = options.data; Object.keys(obj).forEach(key => { const subjects = []; Object.defineProperty(this, key, { get() { if (Observer.target) subjects.push(Observer.target); return obj[key]; }, set(newVal) { if (newVal === obj[key]) return; obj[key] = newVal; // 給當(dāng)前主題所有登記過的觀察者發(fā)出通知 subjects.forEach(observer => observer.update()); } }); }); }
接下來就是遍歷根節(jié)點(這里只遍歷一層),根據(jù)子節(jié)點的類型,傳入不同的參數(shù)調(diào)用 Observer 構(gòu)造函數(shù),然后首次更新視圖,并觸發(fā) getter 將觀察者對象都對應(yīng)放到 options.data 的每個屬性主題中,然后按屬性類型添加不同的事件監(jiān)聽。
const el = document.querySelector(options.el); el.childNodes.forEach(node => { if (node.nodeType === 1) { if (node.hasAttribute('v-model')) { const name = node.getAttribute('v-model'); if (node.type === 'checkbox') node.checked = this[name].includes(node.value); const eventType = (node.tagName === 'INPUT' && node.type === 'text') || node.tagName == 'TEXTAREA' ? 'input' : 'change'; node.addEventListener(eventType, e => { // 這里 this[name] 寫入操作會觸發(fā) setter if (node.type === 'checkbox') { if (node.checked) this[name] = this[name].concat(node.value).sort(); else this[name] = this[name].filter(v => v !== node.value).sort(); } else this[name] = node.value; }); new Observer(this, node, name, 'value'); } else if (node.hasAttribute('v-bind')) { new Observer(this, node, node.getAttribute('v-bind'), 'textContent'); } } else if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.nodeValue)) { new Observer(this, node, RegExp.$1.trim(), 'nodeValue'); } });
<div id="app"> <input type="radio" name="hello" id="hello" value="hello" v-model="title"> <label for="hello">hello</label> <input type="radio" name="hello" id="hello2" value="hello2" v-model="title"> <label for="hello2">hello2</label> <p v-bind="title"></p> <input v-model="content"> <select v-model="content"> <option>world</option> <option>world1</option> <option>world2</option> </select> <p v-bind="content"></p> <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby"> <label for="hobby1">hobby1</label> <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby"> <label for="hobby2">hobby2</label> <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby"> <label for="hobby3">hobby3</label> <br> {{ hobby }} </div>
運行:
Proxy
語法:
new Proxy(target, handler)
- target:被代理的對象
- handler:被代理對象上的自定義行為,和 Reflect 對象的所有靜態(tài)方法對應(yīng),所以可以在其中調(diào)用對應(yīng)的 Reflect 方法,完成默認行為,然后再部署額外的功能。
第一步定義觀察者構(gòu)造函數(shù),和 Object.defineProperty 方式相同。
第二步也是定義 Vue 構(gòu)造函數(shù),不同的是使用 Proxy 劫持屬性的讀取和寫入操作,不需要為 options.data 對象每個屬性都添加主題了。其他和 Object.defineProperty 方式相同。
function Vue(options) { const subjects = []; this.proxy = new Proxy(options.data, { get(obj, key, receiver) { if (Observer.target) subjects.push(Observer.target); const value = Reflect.get(...arguments); return value; }, set(obj, key, value, receiver) { if (value === obj[key]) return; const result = Reflect.set(...arguments); subjects.forEach(observer => observer.update()); return result; } }); }
第三步遍歷根節(jié)點,觸發(fā) getter 將觀察者對象都放到主題的數(shù)組中,然后添加事件監(jiān)聽時,要觸發(fā) Proxy 的寫入操作,而不是原對象。
const el = document.querySelector(options.el); el.childNodes.forEach(node => { if (node.nodeType === 1) { if (node.hasAttribute('v-model')) { const name = node.getAttribute('v-model'); if (node.type === 'checkbox') node.checked = this.proxy[name].includes(node.value); const eventType = (node.tagName === 'INPUT' && node.type === 'text') || node.tagName == 'TEXTAREA' ? 'input' : 'change'; node.addEventListener(eventType, e => { // 這里 this.proxy[name] 寫入操作會觸發(fā) setter if (node.type === 'checkbox') { let value = this.proxy[name]; if (node.checked) { this.proxy[name] = value.concat(node.value).sort(); } else this.proxy[name] = value.filter(v => v !== node.value).sort(); } else this.proxy[name] = node.value; }); new Observer(this.proxy, node, name, 'value'); } else if (node.hasAttribute('v-bind')) { new Observer(this.proxy, node, node.getAttribute('v-bind'), 'textContent'); } } else if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.nodeValue)) { new Observer(this.proxy, node, RegExp.$1.trim(), 'nodeValue'); } });
<div id="app"> <input type="radio" name="hello" id="hello" value="hello" v-model="title"> <label for="hello">hello</label> <input type="radio" name="hello" id="hello2" value="hello2" v-model="title"> <label for="hello2">hello2</label> <p v-bind="title"></p> <input v-model="content"> <select v-model="content"> <option>world</option> <option>world1</option> <option>world2</option> </select> <p v-bind="content"></p> <input type="checkbox" id="hobby1" value="hobby1" v-model="hobby"> <label for="hobby1">hobby1</label> <input type="checkbox" id="hobby2" value="hobby2" v-model="hobby"> <label for="hobby2">hobby2</label> <input type="checkbox" id="hobby3" value="hobby3" v-model="hobby"> <label for="hobby3">hobby3</label> <br> {{ hobby }} </div>
運行:
到此這篇關(guān)于Vue2 與 Vue3 的數(shù)據(jù)綁定原理及實現(xiàn)的文章就介紹到這了,更多相關(guān)Vue數(shù)據(jù)綁定內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue.js項目實戰(zhàn)之多語種網(wǎng)站的功能實現(xiàn)(租車)
這篇文章主要介紹了Vue.js項目實戰(zhàn)之多語種網(wǎng)站(租車)的功能實現(xiàn) ,需要的朋友可以參考下2019-08-08vue實現(xiàn)虛擬滾動渲染成千上萬條數(shù)據(jù)
本文主要介紹了vue實現(xiàn)虛擬滾動渲染成千上萬條數(shù)據(jù),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02