您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue中怎么實(shí)現(xiàn)數(shù)據(jù)雙向綁定的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇Vue中怎么實(shí)現(xiàn)數(shù)據(jù)雙向綁定文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。
在我們使用vue的時(shí)候,當(dāng)數(shù)據(jù)發(fā)生了改變,界面也會(huì)跟著更新,但這并不是理所當(dāng)然的,我們修改數(shù)據(jù)的時(shí)候vue是如何監(jiān)聽數(shù)據(jù)的改變以及當(dāng)數(shù)據(jù)發(fā)生改變的時(shí)候vue如何讓界面刷新的?
當(dāng)我們修改數(shù)據(jù)的時(shí)候vue是通過(guò)es5中的Object.defineProperty
方法來(lái)監(jiān)聽數(shù)據(jù)的改變的,當(dāng)數(shù)據(jù)發(fā)生了改變通過(guò)發(fā)布訂閱模式
統(tǒng)計(jì)訂閱者界面發(fā)生了刷新,這是一種設(shè)計(jì)模式。
下圖,從new Vue開始創(chuàng)建Vue實(shí)例,會(huì)傳入el和data,data會(huì)被傳入一個(gè)觀察者對(duì)象,利用Object.definproperty
將data里數(shù)據(jù)轉(zhuǎn)化成getter/setter進(jìn)行數(shù)據(jù)劫持,data里的每個(gè)屬性都會(huì)創(chuàng)建一個(gè)Dep實(shí)例用來(lái)保存watcher實(shí)例
而el則傳入compile,在compile里進(jìn)行指令的解析,當(dāng)解析到el中使用到data里的數(shù)據(jù)會(huì)觸發(fā)我們的getter,從而將我們的watcher添加到依賴當(dāng)中。當(dāng)數(shù)據(jù)發(fā)生改變的時(shí)候會(huì)觸發(fā)我們的setter發(fā)出依賴通知,通知watcher,watcher接受到通知后去向view發(fā)出通知,讓view去更新
html部分創(chuàng)建一個(gè)id為app的div標(biāo)簽,里面有span和input標(biāo)簽,span標(biāo)簽使用了插值表達(dá)式,input標(biāo)簽使用了v-model
<div class="container" id="app">
<span>內(nèi)容:{{content}}</span>
<input type="text" v-model="content">
</div>
js部分引入了一個(gè)vue.js文件,實(shí)現(xiàn)數(shù)據(jù)雙向綁定的代碼就寫在這里面,然后創(chuàng)建Vue實(shí)例vm,把數(shù)據(jù)掛載到div標(biāo)簽上
const vm=new Vue({
el:'#app',
data:{
content:'請(qǐng)輸入開機(jī)密碼'
}
})
new了一個(gè)Vue實(shí)例很明顯需要用到構(gòu)造函數(shù),在vue的源碼里定義類是使用了function來(lái)定義的,這里我使用ES6的class來(lái)創(chuàng)建這個(gè)Vue實(shí)例
然后設(shè)置constructor
,形參設(shè)為obj_instance,作為new一個(gè)Vue實(shí)例的時(shí)候傳入的對(duì)象,并把傳進(jìn)來(lái)的對(duì)象里的data賦值給實(shí)例里的$data屬性
在javascript里對(duì)象的屬性發(fā)生了變化,需要告訴我們,我們就可以把更改后的屬性更新到dom節(jié)點(diǎn)里,因此初始化實(shí)例的時(shí)候定義一個(gè)監(jiān)聽函數(shù)作為調(diào)用,調(diào)用的時(shí)候傳入需要監(jiān)聽的數(shù)據(jù)
class Vue{//創(chuàng)建Vue實(shí)例
constructor(obj_instance){
this.$data=obj_instance.data
Observer(this.$data)
}
}
function Observer(data_instance){//監(jiān)聽函數(shù)
}
打印一下這個(gè)實(shí)例vm
實(shí)例已經(jīng)創(chuàng)建出來(lái)了但是還需要為$data里的每一個(gè)屬性進(jìn)行監(jiān)聽,要實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽就用到了Object.defineProperty
,Object.defineProperty
可以修改對(duì)象的現(xiàn)有屬性,語(yǔ)法格式為Object.defineProperty(obj, prop, descriptor)
obj:要定義屬性的對(duì)象
prop:要定義或修改的屬性的名稱
descriptor:要定義或修改的屬性描述符
監(jiān)聽對(duì)象里的每一個(gè)屬性,我們使用Object.keys和foreach遍歷對(duì)象里的每一個(gè)屬性并且對(duì)每一個(gè)屬性使用Object.defineProperty進(jìn)行數(shù)據(jù)監(jiān)聽
function Observer(data_instance){
Object.keys(data_instance).forEach(key=>{
Object.defineProperty(data_instance,key,{
enumerable:true,//設(shè)置為true表示屬性可以枚舉
configurable:true,//設(shè)置為true表示屬性描述符可以被改變
get(){},//訪問(wèn)該屬性的時(shí)候觸發(fā),get和set函數(shù)就是數(shù)據(jù)監(jiān)聽的核心
set(){},//修改該屬性的時(shí)候觸發(fā)
})
})
}
在Object.defineProperty
前需要將屬性對(duì)應(yīng)的值存起來(lái)然后在get函數(shù)里面返回出來(lái),不然到了get函數(shù)以后屬性的值已經(jīng)沒了,返回給屬性的值就變成了undefined
let value=data_instance[key]
Object.defineProperty(data_instance,key,{
enumerable:true,
configurable:true,
get(){
console.log(key,value);
return value
},
set(){}
})
點(diǎn)擊一下$data里的屬性名就會(huì)觸發(fā)get函數(shù)
然后設(shè)置set函數(shù),為set設(shè)置形參,這個(gè)形參表示新傳進(jìn)來(lái)的屬性值,然后將這個(gè)新的屬性值賦值給變量value,不需要return返回什么,只做修改,返回是在訪問(wèn)get的時(shí)候返回的,修改之后get也會(huì)訪問(wèn)最新的value變量值
set(newValue){
console.log(key,value,newValue);
value = newValue
}
但是當(dāng)前只為$data的第一層屬性設(shè)置了get和set,如果還有更深的一層如
obj:{
a:'a',
b:'b'
}
這種的并沒有設(shè)置get和set,我們需要一層一層的往屬性里面進(jìn)行數(shù)據(jù)劫持,因此使用遞歸再次監(jiān)聽自己,并在遍歷之前進(jìn)行條件判斷,沒有子屬性了或者沒有檢測(cè)到對(duì)象就終止遞歸
function Observer(data_instance){
//遞歸出口
if(!data_instance || typeof data_instance != 'object') return
Object.keys(data_instance).forEach(key=>{
let value=data_instance[key]
Observer(value)//遞歸-子屬性的劫持
Object.defineProperty(data_instance,key,{
enumerable:true,
configurable:true,
get(){
console.log(key,value);
return value
},
set(newValue){
console.log(key,value,newValue);
value = newValue
}
})
})
}
還有一個(gè)細(xì)節(jié),如果我們將$data的content屬性從字符串改寫成一個(gè)對(duì)象,這個(gè)新的對(duì)象并沒有g(shù)et和set
因?yàn)槲覀冊(cè)谛薷牡臅r(shí)候根本沒有設(shè)置get和set,因此在set里要調(diào)用監(jiān)聽函數(shù)
set(newValue){
console.log(key,value,newValue);
value = newValue
Observer(newValue)
}
劫持?jǐn)?shù)據(jù)后就要把Vue實(shí)例里的數(shù)據(jù)應(yīng)用帶頁(yè)面上,得要加一個(gè)臨時(shí)內(nèi)存區(qū)域,將所有數(shù)據(jù)都更新后再渲染頁(yè)面以此減少dom操作
創(chuàng)建一個(gè)解析函數(shù),設(shè)置2個(gè)參數(shù),一個(gè)是Vue實(shí)例里掛載的元素,另一個(gè)是Vue實(shí)例,在函數(shù)里獲取獲取元素保存在實(shí)例了的$el里,獲取元素后放入臨時(shí)內(nèi)存里,需要用到[createDocumentFragment]
創(chuàng)建一個(gè)新的空白的文檔片段
然后把$el的子節(jié)點(diǎn)一個(gè)一個(gè)加到fragment變量里,頁(yè)面已經(jīng)沒有內(nèi)容了,內(nèi)容都被臨時(shí)存在fragment里了
class Vue{
constructor(obj_instance){
this.$data=obj_instance.data
Observer(this.$data)
Compile(obj_instance.el,this)
}
}
function Compile(ele,vm){
vm.$el=document.querySelector(ele)
const fragment=document.createDocumentFragment()
let child;
while (child=vm.$el.firstChild){
fragment.append(child)
}
console.log(fragment);
console.log(fragment.childNodes);
}
現(xiàn)在直接把需要修改的內(nèi)容應(yīng)用到文檔碎片里面,應(yīng)用后重新渲染,只需修改了fragment的childNodes子節(jié)點(diǎn)的文本節(jié)點(diǎn),文本節(jié)點(diǎn)的類型是3,可以創(chuàng)建一個(gè)函數(shù)并調(diào)用來(lái)修改fragment里的內(nèi)容
節(jié)點(diǎn)里面可能還會(huì)有節(jié)點(diǎn),因此判定節(jié)點(diǎn)類型是否為3,不是就遞歸調(diào)用這個(gè)解析函數(shù)
節(jié)點(diǎn)類型為3就進(jìn)行修改操作,但也不行把整個(gè)節(jié)點(diǎn)的文本都修改,只需修改插值表達(dá)式的內(nèi)容,因此要使用正則表達(dá)式匹配,將匹配的結(jié)果保存到變量里,匹配的結(jié)果是一個(gè)數(shù)組,而索引為1的元素才是我們需要提取出來(lái)的元素,這個(gè)元素就是去除了{(lán){}}和空格得到的字符串,然后就可以直接用Vue實(shí)例來(lái)訪問(wèn)對(duì)應(yīng)屬性的值,修改完后return出去結(jié)束遞歸
function Compile(ele,vm){
vm.$el=document.querySelector(ele) //獲取元素保存在實(shí)例了的$el里
const fragment=document.createDocumentFragment() //創(chuàng)建文檔碎片
let child;
while (child=vm.$el.firstChild){//循環(huán)將子節(jié)點(diǎn)添加到文檔碎片里
fragment.append(child)
}
fragment_compile(fragment)
function fragment_compile(node){ //修改文本節(jié)點(diǎn)內(nèi)容
const pattern = /\{\{\s*(\S*)\s*\}\}/ //檢索字符串中正則表達(dá)式的匹配,用于匹配插值表達(dá)式
if(node.nodeType===3){
const result = pattern.exec(node.nodeValue)
if(result){
console.log('result[1]')
const value=result[1].split('.').reduce(//split將對(duì)象里的屬性分布在數(shù)組里,鏈?zhǔn)降剡M(jìn)行排列;reduce進(jìn)行累加,層層遞進(jìn)獲取$data的值
(total,current)=>total[current],vm.$data
)
node.nodeValue=node.nodeValue.replace(pattern,value) //replace函數(shù)將插值表達(dá)式替換成$data里的屬性的值
}
return
}
node.childNodes.forEach(child=>fragment_compile(child))
}
vm.$el.appendChild(fragment) //將文檔碎片應(yīng)用到對(duì)應(yīng)的dom元素里面
}
頁(yè)面的內(nèi)容又出來(lái)了,插值表達(dá)式替換成了vm實(shí)例里的數(shù)據(jù)
雖然進(jìn)行了數(shù)據(jù)劫持,和將數(shù)據(jù)應(yīng)用到頁(yè)面上,但是數(shù)據(jù)發(fā)生變動(dòng)還不能及時(shí)更新,還需要實(shí)現(xiàn)訂閱發(fā)布者模式
首先創(chuàng)建一個(gè)類用來(lái)收集和通知訂閱者,生成實(shí)例的時(shí)候需要有一個(gè)數(shù)組存放訂閱者的信息,一個(gè)將訂閱者添加到這個(gè)數(shù)組里的方法和一個(gè)通知訂閱者的方法,調(diào)用這個(gè)方法就回去遍歷訂閱者的數(shù)組,讓訂閱者調(diào)用自身的update方法進(jìn)行更新
class Dependency{
constructor(){
this.subscribers=[] //存放訂閱者的信息
}
addSub(sub){
this.subscribers.push(sub) //將訂閱者添加到這個(gè)數(shù)組里
}
notify(){
this.subscribers.forEach(sub=>sub.update()) //遍歷訂閱者的數(shù)組,調(diào)用自身的update函數(shù)進(jìn)行更新
}
}
設(shè)置訂閱者類,需要用到Vue實(shí)例上的屬性,需要Vue實(shí)例和Vue實(shí)例對(duì)應(yīng)的屬性和一個(gè)回調(diào)函數(shù)作為參數(shù),將參數(shù)都賦值給實(shí)例
然后就可以創(chuàng)建訂閱者的update函數(shù),在函數(shù)里調(diào)用傳進(jìn)來(lái)的回調(diào)函數(shù)
class Watcher{
constructor(vm,key,callback){//將參數(shù)都賦值給Watcher實(shí)例
this.vm=vm
this.key=key
this.callback=callback
}
update(){
this.callback()
}
}
替換文檔碎片內(nèi)容的時(shí)候需要告訴訂閱者如何更新,所以訂閱者實(shí)例在模板解析把節(jié)點(diǎn)值替換內(nèi)容的時(shí)候創(chuàng)建,傳入vm實(shí)例,exec匹配成功后的索引值1和回調(diào)函數(shù),將替換文本的執(zhí)行語(yǔ)句復(fù)制到回調(diào)函數(shù)里,通知訂閱者更新的時(shí)候就調(diào)用這個(gè)回調(diào)函數(shù)
回調(diào)函數(shù)里的nodeValue要提前保存,不然替換的內(nèi)容就不是插值表達(dá)式而是替換過(guò)的內(nèi)容
然后就要想辦法將訂閱者存儲(chǔ)到Dependency實(shí)例的數(shù)組里,我們可以在構(gòu)造Watcher實(shí)例的時(shí)候保存實(shí)例到訂閱者數(shù)組里
Dependency.temp=this //設(shè)置一個(gè)臨時(shí)屬性temp
將新的訂閱者添加到訂閱者數(shù)組里且還要將所有的訂閱者都進(jìn)行同樣的操作,那么就可以在觸發(fā)get的時(shí)候?qū)⒂嗛喺咛砑拥接嗛喺邤?shù)組里,為了正確觸發(fā)對(duì)應(yīng)的屬性get,需要用reduce方法對(duì)key進(jìn)行同樣的操作
可以看到控制臺(tái)打印出了Wathcer實(shí)例,每個(gè)實(shí)例都不同,都對(duì)應(yīng)不同的屬性值
Dependency類還沒創(chuàng)建實(shí)例,里面的訂閱者數(shù)組是不存在的,所以要先創(chuàng)建實(shí)例再將訂閱者添加到訂閱者數(shù)組里
修改數(shù)據(jù)的時(shí)候通知訂閱者來(lái)進(jìn)行更新,在set里調(diào)用dependency的通知方法,通知方法就會(huì)去遍數(shù)組,訂閱者執(zhí)行自己的update方法進(jìn)行數(shù)據(jù)更新
但是update調(diào)用回調(diào)函數(shù)缺少設(shè)定形參,依舊使用split和reduce方法獲取屬性值
update(){
const value =this.key.split('.').reduce(
(total,current)=>total[current],this.vm.$data
)
this.callback(value)
}
在控制臺(tái)修改屬性值都修改成功了,頁(yè)面也自動(dòng)更新了
完成了文本的綁定就可以綁定輸入框了,在vue里通過(guò)v-model進(jìn)行綁定,因此要判斷哪個(gè)節(jié)點(diǎn)有v-model,元素節(jié)點(diǎn)的類型是1,可以使用nodeName來(lái)匹配input元素,直接在判斷文本節(jié)點(diǎn)下面進(jìn)行新的判斷
if(node.nodeType===1&&node.nodeName==='INPUT'){
const attr=Array.from(node.attributes)
console.log(attr);
}
節(jié)點(diǎn)名字nodeName為v-model,nodeValue為name,就是數(shù)據(jù)里的屬性名
因此對(duì)這個(gè)數(shù)組進(jìn)行遍歷,匹配到了v-model根據(jù)nodeValue找到對(duì)應(yīng)的屬性值,把屬性值賦值到節(jié)點(diǎn)上,同時(shí)為了在數(shù)據(jù)更新后訂閱者知道更新自己,也要在INPUT節(jié)點(diǎn)里新增Watcher實(shí)例
attr.forEach(i=>{
if(i.nodeName==='v-model'){
const value=i.nodeValue.split('.').reduce(
(total,current)=>total[current],vm.$data
)
node.value=value
new Watcher(vm,i.nodeValue,newValue=>{
node.value=newValue
})
}
})
修改屬性值,頁(yè)面也作出修改
最后剩下用視圖改變數(shù)據(jù),在v-model的節(jié)點(diǎn)上使用addEventListener增加input監(jiān)聽事件就行了
node.addEventListener('input',e=>{
const arr1=i.nodeValue.split('.')
const arr2=arr1.slice(0,arr1.length - 1)
const final=arr2.reduce(
(total,current)=>total[current],vm.$data
)
final[arr1[arr1.length - 1]]=e.target.value
})
關(guān)于“Vue中怎么實(shí)現(xiàn)數(shù)據(jù)雙向綁定”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“Vue中怎么實(shí)現(xiàn)數(shù)據(jù)雙向綁定”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。