Vue雙向資料繫結實現
本篇檔案來記錄Vue雙向資料繫結的實現。
一、知識點
- 什麼是雙向資料繫結(MVVM)?
MVVM分別表示Model View View-Model,即模型(資料訪問層)、檢視(介面)、檢視模型(模型和檢視的通訊),是一種軟體架構模式。
View層接收到互動資訊,通過View-Model更新Model資料,同樣,當Model資料發生變化後(一般是請求後端資料)通知View-Model使得檢視發生更新。從而實現雙向資料監聽,並修改檢視或者模型,這就是MVVM模式。 - Vue是如何實現雙向資料繫結的?
實現雙向資料繫結的關鍵在於如何監聽資料發生了變化,Vue2.x及以前版本通過Javascript
Object.defineProperty()
方法實現。由於Object.defineProperty()
無法監聽到陣列更新(準確來說是通過length增加長度監聽不到),所以Vue3.x使用Proxy
作為新的資料監聽方案,Proxy
可以監聽到整個物件的變更。 Object.defineProperty()
此方法可直接在一個物件上定義一個新的屬性,或者修改物件的現有屬性,並返回此物件,MDN傳送門。// 新增屬性 const obj = {} Object.defineProperty(obj, 'name', { value: 'hua', writable: false }) console.log(obj.name) // hua // getter setter,監聽資料變化就藉助這兩個函式 const obj = {} let age = 18 Object.defineProperty(obj, 'age', { get() { console.log('get data') return age }, set(newValue) { console.log('set data') age = newValue } }) obj.age = 16 // 輸出:set data console.log(obj.age) // 先輸出:get data 再輸出:16
Proxy
此物件用於定義基本操作的自定義行為(如屬性查詢、賦值、列舉、函式呼叫等)MDN傳送門。// 賦值轉發到target const target = {} const p = new Proxy(target, {}) p.name = 'hua' console.log(target.name) // hua // 攔截 const target = {} const p = new Proxy(target, { get(obj, prop) { return obj[prop] }, set(obj, prop, value) { if (prop == 'name' && typeof value == 'string') { obj[prop] = value } return true } }) p.name = 66 console.log(target.name) // undefined p.name = 'hua' console.log(target.name) // hua
二、實現
按照MVVM模式進行分解實現,即先實現View觸發Model更新,再實現Model變化而更新View。
-
建立專案
我們先建立mvvm專案,在專案下建立vue.js
和index.html
,分別作為Vue雙向資料繫結“引擎”和Vue的使用場景。 -
回憶如何使用Vue建立專案
我們先來看以下程式碼:<div id="app"> {{ message }} </div>
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } })
是不是很熟悉,這個是Vue官網提供的宣告式渲染,我們也將以此開始我們Vue雙向資料繫結的開始。
-
建構函式(類)Vue
我們發現,Vue其實就是一個建構函式,接受一個物件作為引數,然後將message的值賦值給了id為app的div,那麼我們來實現function Vue(op) { const el = document.querySelector(op.el) el.innerHTML = op.data.meaasge }
至此,我們可以在頁面上看到效果:
到這裡,我們以及有了一絲絲進步,但是接下來我們要實現在輸入框輸入之後,輸入資訊在div中顯示出來。 -
input輸入改變div中的值
- 改造html
<input /> <div id="app"></div>
- js操作Dom實現
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { el.innerHTML = e.target.value }
至此,我們實現了input輸入改變div內容的需求,但是會發現,這個改變與Vue並沒有關係。那如果與Vue有關我們應該怎麼做呢?我們發現,在Vue建構函式中,為div初始化內容的時候是這樣一段程式碼
el.innerHTML = op.data.meaasge
,也就是說,我們可以通過改變op.data.meaasge
的值達到我們想要的結果。因此,我們可以這樣改進程式碼:const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { app.data.message = e.target.value }
但是,當我們這樣改之後,並沒有達到我們的效果,為什麼呢?因為最終要改變div中的值,還是的藉助
innerHTML
實現,那麼我們在什麼時候去呼叫該方法呢?這裡就需要利用對data
進行劫持來觸發。 -
Object.defineProperty
資料劫持
我們繼續來改造我們的建構函式Vue,監聽data的改變,然後呼叫innerHTML
進行賦值。function observe(obj, el) { if (typeof obj !== 'object') { return } for (let key in obj) { observe(obj[key]) let temp = obj[key] Object.defineProperty(obj, key, { get() { return temp }, set(newValue) { if (temp !== newValue) { console.log('data changed', newValue) temp = newValue el.innerHTML = temp } } }) } } function Vue(op) { const el = document.querySelector(op.el) this.data = op.data // 資料監聽 observe(op.data, el) // 初始化 el.innerHTML = op.data.message }
至此,我們實現了通過監聽data變化而改變div內容的需求,但是你會發現,這裡只要有資料發生變化了,都會改變div中的內容,如果我們只想在message改變的時候才改變div中的內容,怎麼辦呢?接下來我們來引入我們的監聽者Watcher,完善我們的Vue.
-
訂閱者Watcher、訂閱器Dep、攔截器observe
// 攔截器 function observe(obj, el) { if (typeof obj !== 'object') { return } for (let key in obj) { observe(obj[key]) let temp = obj[key] let dep = new Dep() Object.defineProperty(obj, key, { get() { // 將watcher新增到訂閱器中 dep.depend() return temp }, set(newValue) { if (temp !== newValue) { temp = newValue dep.notify() } } }) } } // 會有多個訂閱者,所以使用訂閱器進行管理 class Dep { constructor() { this.subs = [] } // 新增訂閱者 depend() { if (Dep.target) { this.addSubs(Dep.target) } } // 通知訂閱者更新 notify() { this.subs.forEach(item => { item.update() }) } addSubs(sub) { this.subs.push(sub) } } Dep.target = null // 訂閱者 class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb this.value = this.get() } // 訂閱者更新檢視 update() { const newValue = this.vm.data[this.key] const oldValue = this.value if (newValue !== oldValue) { // 這裡相當於將cb新增到vm物件,然後進行呼叫 this.cb.call(this.vm, newValue, oldValue) } } // 初始化value,並利用閉包形式將watcher繫結到訂閱器中 get () { Dep.target = this const value = this.vm.data[this.key] Dep.target = null return value } } function Vue(op) { const el = document.querySelector(op.el) this.data = op.data // 資料監聽 observe(op.data, el) // 初始化 el.innerHTML = op.data.message // 監聽message變化,只有message變化才能觸發 new Watcher(this, 'message', function(newValue, ondValue) { // 在這裡對我們的div進行內容賦值 el.innerHTML = newValue }) }
到這裡,我們的Vue雙向資料繫結已經完成了,讓我們一起測試我們的程式碼吧!
-
測試
- View改變Model
輸入框輸入,div內容隨之變化 - Model改變View
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { app.data.message = e.target.value } setTimeout(function() { app.data.message = '過了兩秒執行' }, 2000)
- View改變Model