vue實現(xiàn)簡易的雙向數(shù)據(jù)綁定
主要是通過數(shù)據(jù)劫持和發(fā)布訂閱一起實現(xiàn)的
- 雙向數(shù)據(jù)綁定 數(shù)據(jù)更新時,可以更新視圖 視圖的數(shù)據(jù)更新是,可以反向更新模型
組成說明
- Observe監(jiān)聽器 劫持數(shù)據(jù), 感知數(shù)據(jù)變化, 發(fā)出通知給訂閱者, 在get中將訂閱者添加到訂閱器中
- Dep消息訂閱器 存儲訂閱者, 通知訂閱者調(diào)用更新函數(shù)
- 訂閱者Wather取出模型值,更新視圖
- 解析器Compile 解析指令, 更新模板數(shù)據(jù), 初始化視圖, 實例化一個訂閱者, 將更新函數(shù)綁定到訂閱者上, 可以在接收通知二次更新視圖, 對于v-model還需要監(jiān)聽input事件,實現(xiàn)視圖到模型的數(shù)據(jù)流動
基本結(jié)構(gòu)
HTML模板
<div id="app"> <form> <input type="text" v-model="username"> </form> <p v-bind="username"></p> </div>
- 一個根節(jié)點#app
- 表單元素,里面包含input, 使用v-model指令綁定數(shù)據(jù)username
- p元素上使用v-bind綁定數(shù)username
MyVue類
簡單的模擬Vue類
將實例化時的選項options, 數(shù)據(jù)options.data進行保存 此外,通過options.el獲取dom元素,存儲到$el上
class MyVue { constructor(options) { this.$options = options this.$el = document.querySelector(this.$options.el) this.$data = options.data } }
實例化MyVue
實例化一個MyVue,傳遞選項進去,選項中指定綁定的元素el和數(shù)據(jù)對象data
const myVm = new MyVue({ el: '#app', data: { username: 'LastStarDust' } })
Observe監(jiān)聽器實現(xiàn)
劫持數(shù)據(jù)是為了修改數(shù)據(jù)的時候可以感知, 發(fā)出通知, 執(zhí)行更新視圖操作
class MyVue { constructor(options) { // ... // 監(jiān)視數(shù)據(jù)的屬性 this.observable(this.$data) } // 遞歸遍歷數(shù)據(jù)對象的所有屬性, 進行數(shù)據(jù)屬性的劫持 { username: 'LastStarDust' } observable(obj) { // obj為空或者不是對象, 不做任何操作 const isEmpty = !obj || typeof obj !== 'object' if(isEmpty) { return } // ['username'] const keys = Object.keys(obj) keys.forEach(key => { // 如果屬性值是對象,遞歸調(diào)用 let val = obj[key] if(typeof val === 'object') { this.observable(val) } // this.defineReactive(this.$data, 'username', 'LastStarDust') this.defineReactive(obj, key, val) }) return obj } // 數(shù)據(jù)劫持,修改屬性的get和set方法 defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log(`取出${key}屬性值: 值為${val}`) return val }, set(newVal) { // 沒有發(fā)生變化, 不做更新 if(newVal === val) { return } console.log(`更新屬性${key}的值為: ${newVal}`) val = newVal } }) } }
Dep消息訂閱器
存儲訂閱者, 收到通知時,取出訂閱者,調(diào)用訂閱者的update方法
// 定義消息訂閱器 class Dep { // 靜態(tài)屬性 Dep.target,這是一個全局唯一 的Watcher,因為在同一時間只能有一個全局的 Watcher static target = null constructor() { // 存儲訂閱者 this.subs = [] } // 添加訂閱者 add(sub) { this.subs.push(sub) } // 通知 notify() { this.subs.forEach(sub => { // 調(diào)用訂閱者的update方法 sub.update() }) } }
將消息訂閱器添加到數(shù)據(jù)劫持過程中
為每一個屬性添加訂閱者
defineReactive(obj, key, val) { const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 會在初始化時, 觸發(fā)屬性get()方法,來到這里Dep.target有值,將其作為訂閱者存儲起來,在觸發(fā)屬性的set()方法時,調(diào)用notify方法 if(Dep.target) { dep.add(Dep.target) } console.log(`取出${key}屬性值: 值為${val}`) return val }, set(newVal) { // 沒有發(fā)生變化, 不做更新 if(newVal === val) { return } console.log(`更新屬性${key}的值為: ${newVal}`) val = newVal dep.notify() } }) }
訂閱者Wather
從模型中取出數(shù)據(jù)并更新視圖
// 定義訂閱者類 class Wather { constructor(vm, exp, cb) { this.vm = vm // vm實例 this.exp = exp // 指令對應的字符串值, 如v-model="username", exp相當于"username" this.cb = cb // 回到函數(shù) 更新視圖時調(diào)用 this.value = this.get() // 將自己添加到消息訂閱器Dep中 } get() { // 將當前訂閱者作為全局唯一的Wather,添加到Dep.target上 Dep.target = this // 獲取數(shù)據(jù),觸發(fā)屬性的getter方法 const value = this.vm.$data[this.exp] // 在執(zhí)行添加到消息訂閱Dep后, 重置Dep.target Dep.target = null return value } // 執(zhí)行更新 update() { this.run() } run() { // 從Model模型中取出屬性值 const newVal = this.vm.$data[this.exp] const oldVal = this.value if(newVal === oldVal) { return false } // 執(zhí)行回調(diào)函數(shù), 將vm實例,新值,舊值傳遞過去 this.cb.call(this.vm, newVal, oldVal) } }
解析器Compile
- 解析模板指令,并替換模板數(shù)據(jù),初始化視圖;
- 將模板指令對應的節(jié)點綁定對應的更新函數(shù),初始化相應的訂閱器;
- 初始化編譯器, 存儲el對應的dom元素, 存儲vm實例, 調(diào)用初始化方法
- 在初始化方法中, 從根節(jié)點開始, 取出根節(jié)點的所有子節(jié)點, 逐個對節(jié)點進行解析
- 解析節(jié)點過程中
- 解析指令存在, 取出綁定值, 替換模板數(shù)據(jù), 完成首次視圖的初始化
- 給指令對應的節(jié)點綁定更新函數(shù), 并實例化一個訂閱器Wather
- 對于v-model指令, 監(jiān)聽'input'事件,實現(xiàn)視圖更新是,去更新模型的數(shù)據(jù)
// 定義解析器 // 解析指令,替換模板數(shù)據(jù),初始視圖 // 模板的指令綁定更新函數(shù), 數(shù)據(jù)更新時, 更新視圖 class Compile { constructor(el, vm) { this.el = el this.vm = vm this.init(this.el) } init(el) { this.compileEle(el) } compileEle(ele) { const nodes = ele.children // 遍歷節(jié)點進行解析 for(const node of nodes) { // 如果有子節(jié)點,遞歸調(diào)用 if(node.children && node.children.length !== 0) { this.compileEle(node) } // 指令時v-model并且是標簽是輸入標簽 const hasVmodel = node.hasAttribute('v-model') const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1 if(hasVmodel && isInputTag) { const exp = node.getAttribute('v-model') const val = this.vm.$data[exp] const attr = 'value' // 初次模型值推到視圖層,初始化視圖 this.modelToView(node, val, attr) // 實例化一個訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來數(shù)據(jù)更新,可以更新視圖 new Wather(this.vm, exp, (newVal)=> { this.modelToView(node, newVal, attr) }) // 監(jiān)聽視圖的改變 node.addEventListener('input', (e) => { this.viewToModel(exp, e.target.value) }) } // 指令時v-bind if(node.hasAttribute('v-bind')) { const exp = node.getAttribute('v-bind') const val = this.vm.$data[exp] const attr = 'innerHTML' // 初次模型值推到視圖層,初始化視圖 this.modelToView(node, val, attr) // 實例化一個訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來數(shù)據(jù)更新,可以更新視圖 new Wather(this.vm, exp, (newVal)=> { this.modelToView(node, newVal, attr) }) } } } // 將模型值更新到視圖 modelToView(node, val, attr) { node[attr] = val } // 將視圖值更新到模型上 viewToModel(exp, val) { this.vm.$data[exp] = val } }
完整代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <form> <input type="text" v-model="username"> </form> <div> <span v-bind="username"></span> </div> <p v-bind="username"></p> </div> <script> class MyVue { constructor(options) { this.$options = options this.$el = document.querySelector(this.$options.el) this.$data = options.data // 監(jiān)視數(shù)據(jù)的屬性 this.observable(this.$data) // 編譯節(jié)點 new Compile(this.$el, this) } // 遞歸遍歷數(shù)據(jù)對象的所有屬性, 進行數(shù)據(jù)屬性的劫持 { username: 'LastStarDust' } observable(obj) { // obj為空或者不是對象, 不做任何操作 const isEmpty = !obj || typeof obj !== 'object' if(isEmpty) { return } // ['username'] const keys = Object.keys(obj) keys.forEach(key => { // 如果屬性值是對象,遞歸調(diào)用 let val = obj[key] if(typeof val === 'object') { this.observable(val) } // this.defineReactive(this.$data, 'username', 'LastStarDust') this.defineReactive(obj, key, val) }) return obj } // 數(shù)據(jù)劫持,修改屬性的get和set方法 defineReactive(obj, key, val) { const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 會在初始化時, 觸發(fā)屬性get()方法,來到這里Dep.target有值,將其作為訂閱者存儲起來,在觸發(fā)屬性的set()方法時,調(diào)用notify方法 if(Dep.target) { dep.add(Dep.target) } console.log(`取出${key}屬性值: 值為${val}`) return val }, set(newVal) { // 沒有發(fā)生變化, 不做更新 if(newVal === val) { return } console.log(`更新屬性${key}的值為: ${newVal}`) val = newVal dep.notify() } }) } } // 定義消息訂閱器 class Dep { // 靜態(tài)屬性 Dep.target,這是一個全局唯一 的Watcher,因為在同一時間只能有一個全局的 Watcher static target = null constructor() { // 存儲訂閱者 this.subs = [] } // 添加訂閱者 add(sub) { this.subs.push(sub) } // 通知 notify() { this.subs.forEach(sub => { // 調(diào)用訂閱者的update方法 sub.update() }) } } // 定義訂閱者類 class Wather { constructor(vm, exp, cb) { this.vm = vm // vm實例 this.exp = exp // 指令對應的字符串值, 如v-model="username", exp相當于"username" this.cb = cb // 回到函數(shù) 更新視圖時調(diào)用 this.value = this.get() // 將自己添加到消息訂閱器Dep中 } get() { // 將當前訂閱者作為全局唯一的Wather,添加到Dep.target上 Dep.target = this // 獲取數(shù)據(jù),觸發(fā)屬性的getter方法 const value = this.vm.$data[this.exp] // 在執(zhí)行添加到消息訂閱Dep后, 重置Dep.target Dep.target = null return value } // 執(zhí)行更新 update() { this.run() } run() { // 從Model模型中取出屬性值 const newVal = this.vm.$data[this.exp] const oldVal = this.value if(newVal === oldVal) { return false } // 執(zhí)行回調(diào)函數(shù), 將vm實例,新值,舊值傳遞過去 this.cb.call(this.vm, newVal, oldVal) } } // 定義解析器 // 解析指令,替換模板數(shù)據(jù),初始視圖 // 模板的指令綁定更新函數(shù), 數(shù)據(jù)更新時, 更新視圖 class Compile { constructor(el, vm) { this.el = el this.vm = vm this.init(this.el) } init(el) { this.compileEle(el) } compileEle(ele) { const nodes = ele.children for(const node of nodes) { if(node.children && node.children.length !== 0) { // 遞歸調(diào)用, 編譯子節(jié)點 this.compileEle(node) } // 指令時v-model并且是標簽是輸入標簽 const hasVmodel = node.hasAttribute('v-model') const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1 if(hasVmodel && isInputTag) { const exp = node.getAttribute('v-model') const val = this.vm.$data[exp] const attr = 'value' // 初次模型值推到視圖層,初始化視圖 this.modelToView(node, val, attr) // 實例化一個訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來數(shù)據(jù)更新,可以更新視圖 new Wather(this.vm, exp, (newVal)=> { this.modelToView(node, newVal, attr) }) // 監(jiān)聽視圖的改變 node.addEventListener('input', (e) => { this.viewToModel(exp, e.target.value) }) } if(node.hasAttribute('v-bind')) { const exp = node.getAttribute('v-bind') const val = this.vm.$data[exp] const attr = 'innerHTML' // 初次模型值推到視圖層,初始化視圖 this.modelToView(node, val, attr) // 實例化一個訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來數(shù)據(jù)更新,可以更新視圖 new Wather(this.vm, exp, (newVal)=> { this.modelToView(node, newVal, attr) }) } } } // 將模型值更新到視圖 modelToView(node, val, attr) { node[attr] = val } // 將視圖值更新到模型上 viewToModel(exp, val) { this.vm.$data[exp] = val } } const myVm = new MyVue({ el: '#app', data: { username: 'LastStarDust' } }) // console.log(Dep.target) </script> </body> </html>
以上就是vue實現(xiàn)簡易的雙向數(shù)據(jù)綁定的詳細內(nèi)容,更多關于vue 實現(xiàn)雙向數(shù)據(jù)綁定的資料請關注腳本之家其它相關文章!
相關文章
IDEA中Debug調(diào)試VUE前端項目調(diào)試JS只需兩步
這篇文章主要為大家介紹了在IDEA中Debug調(diào)試VUE前端項目,只需要兩步就可以調(diào)試JS的實現(xiàn)方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-02-02vue中國城市選擇器的使用教程(element-china-area-data)
這篇文章主要給大家介紹了關于vue中國城市選擇器使用(element-china-area-data)的相關資料,使用element-china-area-data插件可以非常方便地實現(xiàn)省市縣三級聯(lián)動選擇器,需要的朋友可以參考下2023-11-11vue項目中的遇錯:Invalid?Host?header問題
這篇文章主要介紹了vue項目中的遇錯:Invalid?Host?header問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07