vue實(shí)現(xiàn)簡(jiǎn)易的雙向數(shù)據(jù)綁定
主要是通過(guò)數(shù)據(jù)劫持和發(fā)布訂閱一起實(shí)現(xiàn)的
- 雙向數(shù)據(jù)綁定 數(shù)據(jù)更新時(shí),可以更新視圖 視圖的數(shù)據(jù)更新是,可以反向更新模型
組成說(shuō)明
- Observe監(jiān)聽(tīng)器 劫持?jǐn)?shù)據(jù), 感知數(shù)據(jù)變化, 發(fā)出通知給訂閱者, 在get中將訂閱者添加到訂閱器中
- Dep消息訂閱器 存儲(chǔ)訂閱者, 通知訂閱者調(diào)用更新函數(shù)
- 訂閱者Wather取出模型值,更新視圖
- 解析器Compile 解析指令, 更新模板數(shù)據(jù), 初始化視圖, 實(shí)例化一個(gè)訂閱者, 將更新函數(shù)綁定到訂閱者上, 可以在接收通知二次更新視圖, 對(duì)于v-model還需要監(jiān)聽(tīng)input事件,實(shí)現(xiàn)視圖到模型的數(shù)據(jù)流動(dòng)
基本結(jié)構(gòu)
HTML模板
<div id="app"> <form> <input type="text" v-model="username"> </form> <p v-bind="username"></p> </div>
- 一個(gè)根節(jié)點(diǎn)#app
- 表單元素,里面包含input, 使用v-model指令綁定數(shù)據(jù)username
- p元素上使用v-bind綁定數(shù)username
MyVue類(lèi)
簡(jiǎn)單的模擬Vue類(lèi)
將實(shí)例化時(shí)的選項(xiàng)options, 數(shù)據(jù)options.data進(jìn)行保存 此外,通過(guò)options.el獲取dom元素,存儲(chǔ)到$el上
class MyVue {
constructor(options) {
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.$data = options.data
}
}
實(shí)例化MyVue
實(shí)例化一個(gè)MyVue,傳遞選項(xiàng)進(jìn)去,選項(xiàng)中指定綁定的元素el和數(shù)據(jù)對(duì)象data
const myVm = new MyVue({
el: '#app',
data: {
username: 'LastStarDust'
}
})
Observe監(jiān)聽(tīng)器實(shí)現(xiàn)
劫持?jǐn)?shù)據(jù)是為了修改數(shù)據(jù)的時(shí)候可以感知, 發(fā)出通知, 執(zhí)行更新視圖操作
class MyVue {
constructor(options) {
// ...
// 監(jiān)視數(shù)據(jù)的屬性
this.observable(this.$data)
}
// 遞歸遍歷數(shù)據(jù)對(duì)象的所有屬性, 進(jìn)行數(shù)據(jù)屬性的劫持 { username: 'LastStarDust' }
observable(obj) {
// obj為空或者不是對(duì)象, 不做任何操作
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// 如果屬性值是對(duì)象,遞歸調(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) {
// 沒(méi)有發(fā)生變化, 不做更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值為: ${newVal}`)
val = newVal
}
})
}
}
Dep消息訂閱器
存儲(chǔ)訂閱者, 收到通知時(shí),取出訂閱者,調(diào)用訂閱者的update方法
// 定義消息訂閱器
class Dep {
// 靜態(tài)屬性 Dep.target,這是一個(gè)全局唯一 的Watcher,因?yàn)樵谕粫r(shí)間只能有一個(gè)全局的 Watcher
static target = null
constructor() {
// 存儲(chǔ)訂閱者
this.subs = []
}
// 添加訂閱者
add(sub) {
this.subs.push(sub)
}
// 通知
notify() {
this.subs.forEach(sub => {
// 調(diào)用訂閱者的update方法
sub.update()
})
}
}
將消息訂閱器添加到數(shù)據(jù)劫持過(guò)程中
為每一個(gè)屬性添加訂閱者
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 會(huì)在初始化時(shí), 觸發(fā)屬性get()方法,來(lái)到這里Dep.target有值,將其作為訂閱者存儲(chǔ)起來(lái),在觸發(fā)屬性的set()方法時(shí),調(diào)用notify方法
if(Dep.target) {
dep.add(Dep.target)
}
console.log(`取出${key}屬性值: 值為${val}`)
return val
},
set(newVal) {
// 沒(méi)有發(fā)生變化, 不做更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值為: ${newVal}`)
val = newVal
dep.notify()
}
})
}
訂閱者Wather
從模型中取出數(shù)據(jù)并更新視圖
// 定義訂閱者類(lèi)
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm實(shí)例
this.exp = exp // 指令對(duì)應(yīng)的字符串值, 如v-model="username", exp相當(dāng)于"username"
this.cb = cb // 回到函數(shù) 更新視圖時(shí)調(diào)用
this.value = this.get() // 將自己添加到消息訂閱器Dep中
}
get() {
// 將當(dāng)前訂閱者作為全局唯一的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實(shí)例,新值,舊值傳遞過(guò)去
this.cb.call(this.vm, newVal, oldVal)
}
}
解析器Compile
- 解析模板指令,并替換模板數(shù)據(jù),初始化視圖;
- 將模板指令對(duì)應(yīng)的節(jié)點(diǎn)綁定對(duì)應(yīng)的更新函數(shù),初始化相應(yīng)的訂閱器;
- 初始化編譯器, 存儲(chǔ)el對(duì)應(yīng)的dom元素, 存儲(chǔ)vm實(shí)例, 調(diào)用初始化方法
- 在初始化方法中, 從根節(jié)點(diǎn)開(kāi)始, 取出根節(jié)點(diǎn)的所有子節(jié)點(diǎn), 逐個(gè)對(duì)節(jié)點(diǎn)進(jìn)行解析
- 解析節(jié)點(diǎn)過(guò)程中
- 解析指令存在, 取出綁定值, 替換模板數(shù)據(jù), 完成首次視圖的初始化
- 給指令對(duì)應(yīng)的節(jié)點(diǎn)綁定更新函數(shù), 并實(shí)例化一個(gè)訂閱器Wather
- 對(duì)于v-model指令, 監(jiān)聽(tīng)'input'事件,實(shí)現(xiàn)視圖更新是,去更新模型的數(shù)據(jù)
// 定義解析器
// 解析指令,替換模板數(shù)據(jù),初始視圖
// 模板的指令綁定更新函數(shù), 數(shù)據(jù)更新時(shí), 更新視圖
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é)點(diǎn)進(jìn)行解析
for(const node of nodes) {
// 如果有子節(jié)點(diǎn),遞歸調(diào)用
if(node.children && node.children.length !== 0) {
this.compileEle(node)
}
// 指令時(shí)v-model并且是標(biāo)簽是輸入標(biāo)簽
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í)例化一個(gè)訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來(lái)數(shù)據(jù)更新,可以更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// 監(jiān)聽(tīng)視圖的改變
node.addEventListener('input', (e) => {
this.viewToModel(exp, e.target.value)
})
}
// 指令時(shí)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í)例化一個(gè)訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來(lái)數(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é)點(diǎn)
new Compile(this.$el, this)
}
// 遞歸遍歷數(shù)據(jù)對(duì)象的所有屬性, 進(jìn)行數(shù)據(jù)屬性的劫持 { username: 'LastStarDust' }
observable(obj) {
// obj為空或者不是對(duì)象, 不做任何操作
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// 如果屬性值是對(duì)象,遞歸調(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() {
// 會(huì)在初始化時(shí), 觸發(fā)屬性get()方法,來(lái)到這里Dep.target有值,將其作為訂閱者存儲(chǔ)起來(lái),在觸發(fā)屬性的set()方法時(shí),調(diào)用notify方法
if(Dep.target) {
dep.add(Dep.target)
}
console.log(`取出${key}屬性值: 值為${val}`)
return val
},
set(newVal) {
// 沒(méi)有發(fā)生變化, 不做更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值為: ${newVal}`)
val = newVal
dep.notify()
}
})
}
}
// 定義消息訂閱器
class Dep {
// 靜態(tài)屬性 Dep.target,這是一個(gè)全局唯一 的Watcher,因?yàn)樵谕粫r(shí)間只能有一個(gè)全局的 Watcher
static target = null
constructor() {
// 存儲(chǔ)訂閱者
this.subs = []
}
// 添加訂閱者
add(sub) {
this.subs.push(sub)
}
// 通知
notify() {
this.subs.forEach(sub => {
// 調(diào)用訂閱者的update方法
sub.update()
})
}
}
// 定義訂閱者類(lèi)
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm實(shí)例
this.exp = exp // 指令對(duì)應(yīng)的字符串值, 如v-model="username", exp相當(dāng)于"username"
this.cb = cb // 回到函數(shù) 更新視圖時(shí)調(diào)用
this.value = this.get() // 將自己添加到消息訂閱器Dep中
}
get() {
// 將當(dāng)前訂閱者作為全局唯一的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實(shí)例,新值,舊值傳遞過(guò)去
this.cb.call(this.vm, newVal, oldVal)
}
}
// 定義解析器
// 解析指令,替換模板數(shù)據(jù),初始視圖
// 模板的指令綁定更新函數(shù), 數(shù)據(jù)更新時(shí), 更新視圖
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é)點(diǎn)
this.compileEle(node)
}
// 指令時(shí)v-model并且是標(biāo)簽是輸入標(biāo)簽
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í)例化一個(gè)訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來(lái)數(shù)據(jù)更新,可以更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// 監(jiān)聽(tīng)視圖的改變
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í)例化一個(gè)訂閱者, 將更新函數(shù)綁定到訂閱者上, 未來(lái)數(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實(shí)現(xiàn)簡(jiǎn)易的雙向數(shù)據(jù)綁定的詳細(xì)內(nèi)容,更多關(guān)于vue 實(shí)現(xiàn)雙向數(shù)據(jù)綁定的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Vue響應(yīng)式原理及雙向數(shù)據(jù)綁定示例分析
- 深入了解Vue中雙向數(shù)據(jù)綁定原理
- vue實(shí)現(xiàn)雙向數(shù)據(jù)綁定
- Vue雙向數(shù)據(jù)綁定(MVVM)的原理
- vue中的雙向數(shù)據(jù)綁定原理與常見(jiàn)操作技巧詳解
- Vue的雙向數(shù)據(jù)綁定實(shí)現(xiàn)原理解析
- Vue組件內(nèi)部實(shí)現(xiàn)一個(gè)雙向數(shù)據(jù)綁定的實(shí)例代碼
- vue實(shí)現(xiàn)的雙向數(shù)據(jù)綁定操作示例
- Vue雙向數(shù)據(jù)綁定與響應(yīng)式原理深入探究
相關(guān)文章
vue中插件和組件的區(qū)別點(diǎn)及用法總結(jié)
在本篇文章里小編給大家分享的是一篇關(guān)于vue中插件和組件的區(qū)別點(diǎn)及用法總結(jié)內(nèi)容,有興趣的的朋友們可以學(xué)習(xí)下。2021-12-12
IDEA中Debug調(diào)試VUE前端項(xiàng)目調(diào)試JS只需兩步
這篇文章主要為大家介紹了在IDEA中Debug調(diào)試VUE前端項(xiàng)目,只需要兩步就可以調(diào)試JS的實(shí)現(xiàn)方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-02-02
Vue封裝Swiper實(shí)現(xiàn)圖片輪播效果
圖片輪播是前端中經(jīng)常需要實(shí)現(xiàn)的一個(gè)功能。最近學(xué)習(xí)Vue.js,就針對(duì)Swiper進(jìn)行封裝,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的圖片輪播組件。感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-02-02
vue中國(guó)城市選擇器的使用教程(element-china-area-data)
這篇文章主要給大家介紹了關(guān)于vue中國(guó)城市選擇器使用(element-china-area-data)的相關(guān)資料,使用element-china-area-data插件可以非常方便地實(shí)現(xiàn)省市縣三級(jí)聯(lián)動(dòng)選擇器,需要的朋友可以參考下2023-11-11
vue項(xiàng)目中的遇錯(cuò):Invalid?Host?header問(wèn)題
這篇文章主要介紹了vue項(xiàng)目中的遇錯(cuò):Invalid?Host?header問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
基于vue.js實(shí)現(xiàn)分頁(yè)查詢(xún)功能
這篇文章主要為大家詳細(xì)介紹了基于vue.js實(shí)現(xiàn)分頁(yè)查詢(xún)功能,vue.js實(shí)現(xiàn)數(shù)據(jù)庫(kù)分頁(yè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12

