Vue中如何實現(xiàn)數(shù)據(jù)雙向綁定?下面本篇文章給大家介紹一下Vue.js數(shù)據(jù)雙向綁定的實現(xiàn)方法,希望對大家有所幫助!
前端(vue)入門到精通課程,老師在線輔導:聯(lián)系老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點擊使用
在我們使用vue的時候,當數(shù)據(jù)發(fā)生了改變,界面也會跟著更新,但這并不是理所當然的,我們修改數(shù)據(jù)的時候vue是如何監(jiān)聽數(shù)據(jù)的改變以及當數(shù)據(jù)發(fā)生改變的時候vue如何讓界面刷新的?
當我們修改數(shù)據(jù)的時候vue是通過es5中的Object.defineProperty
方法來監(jiān)聽數(shù)據(jù)的改變的,當數(shù)據(jù)發(fā)生了改變通過發(fā)布訂閱模式
統(tǒng)計訂閱者界面發(fā)生了刷新,這是一種設(shè)計模式。【學習視頻分享:vue視頻教程、web前端視頻】
下圖,從new Vue開始創(chuàng)建Vue實例,會傳入el和data,data會被傳入一個觀察者對象,利用Object.definproperty
將data里數(shù)據(jù)轉(zhuǎn)化成getter/setter進行數(shù)據(jù)劫持,data里的每個屬性都會創(chuàng)建一個Dep實例用來保存watcher實例
而el則傳入compile,在compile里進行指令的解析,當解析到el中使用到data里的數(shù)據(jù)會觸發(fā)我們的getter,從而將我們的watcher添加到依賴當中。當數(shù)據(jù)發(fā)生改變的時候會觸發(fā)我們的setter發(fā)出依賴通知,通知watcher,watcher接受到通知后去向view發(fā)出通知,讓view去更新
數(shù)據(jù)劫持
html部分創(chuàng)建一個id為app的div標簽,里面有span和input標簽,span標簽使用了插值表達式,input標簽使用了v-model
<div class="container" id="app"> <span>內(nèi)容:{{content}}</span> <input type="text" v-model="content"> </div>
js部分引入了一個vue.js文件,實現(xiàn)數(shù)據(jù)雙向綁定的代碼就寫在這里面,然后創(chuàng)建Vue實例vm,把數(shù)據(jù)掛載到div標簽上
const vm=new Vue({ el:'#app', data:{ content:'請輸入開機密碼' } })
new了一個Vue實例很明顯需要用到構(gòu)造函數(shù),在vue的源碼里定義類是使用了function來定義的,這里我使用ES6的class來創(chuàng)建這個Vue實例
然后設(shè)置constructor
,形參設(shè)為obj_instance,作為new一個Vue實例的時候傳入的對象,并把傳進來的對象里的data賦值給實例里的$data屬性
在javascript里對象的屬性發(fā)生了變化,需要告訴我們,我們就可以把更改后的屬性更新到dom節(jié)點里,因此初始化實例的時候定義一個監(jiān)聽函數(shù)作為調(diào)用,調(diào)用的時候傳入需要監(jiān)聽的數(shù)據(jù)
class Vue{//創(chuàng)建Vue實例 constructor(obj_instance){ this.$data=obj_instance.data Observer(this.$data) } } function Observer(data_instance){//監(jiān)聽函數(shù) }
打印一下這個實例vm
實例已經(jīng)創(chuàng)建出來了但是還需要為$data里的每一個屬性進行監(jiān)聽,要實現(xiàn)數(shù)據(jù)監(jiān)聽就用到了Object.defineProperty
,Object.defineProperty
可以修改對象的現(xiàn)有屬性,語法格式為Object.defineProperty(obj, prop, descriptor)
- obj:要定義屬性的對象
- prop:要定義或修改的屬性的名稱
- descriptor:要定義或修改的屬性描述符
監(jiān)聽對象里的每一個屬性,我們使用Object.keys和foreach遍歷對象里的每一個屬性并且對每一個屬性使用Object.defineProperty進行數(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(){},//訪問該屬性的時候觸發(fā),get和set函數(shù)就是數(shù)據(jù)監(jiān)聽的核心 set(){},//修改該屬性的時候觸發(fā) }) }) }
在Object.defineProperty
前需要將屬性對應(yīng)的值存起來然后在get函數(shù)里面返回出來,不然到了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(){} })
點擊一下$data里的屬性名就會觸發(fā)get函數(shù)
然后設(shè)置set函數(shù),為set設(shè)置形參,這個形參表示新傳進來的屬性值,然后將這個新的屬性值賦值給變量value,不需要return返回什么,只做修改,返回是在訪問get的時候返回的,修改之后get也會訪問最新的value變量值
set(newValue){ console.log(key,value,newValue); value = newValue }
但是當前只為$data的第一層屬性設(shè)置了get和set,如果還有更深的一層如
obj:{ a:'a', b:'b' }
這種的并沒有設(shè)置get和set,我們需要一層一層的往屬性里面進行數(shù)據(jù)劫持,因此使用遞歸再次監(jiān)聽自己,并在遍歷之前進行條件判斷,沒有子屬性了或者沒有檢測到對象就終止遞歸
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 } }) }) }
還有一個細節(jié),如果我們將$data的content屬性從字符串改寫成一個對象,這個新的對象并沒有g(shù)et和set
因為我們在修改的時候根本沒有設(shè)置get和set,因此在set里要調(diào)用監(jiān)聽函數(shù)
set(newValue){ console.log(key,value,newValue); value = newValue Observer(newValue) }
模板解析
劫持數(shù)據(jù)后就要把Vue實例里的數(shù)據(jù)應(yīng)用帶頁面上,得要加一個臨時內(nèi)存區(qū)域,將所有數(shù)據(jù)都更新后再渲染頁面以此減少dom操作
創(chuàng)建一個解析函數(shù),設(shè)置2個參數(shù),一個是Vue實例里掛載的元素,另一個是Vue實例,在函數(shù)里獲取獲取元素保存在實例了的$el里,獲取元素后放入臨時內(nèi)存里,需要用到[createDocumentFragment]
創(chuàng)建一個新的空白的文檔片段
然后把$el的子節(jié)點一個一個加到fragment變量里,頁面已經(jīng)沒有內(nèi)容了,內(nèi)容都被臨時存在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é)點的文本節(jié)點,文本節(jié)點的類型是3,可以創(chuàng)建一個函數(shù)并調(diào)用來修改fragment里的內(nèi)容
節(jié)點里面可能還會有節(jié)點,因此判定節(jié)點類型是否為3,不是就遞歸調(diào)用這個解析函數(shù)
節(jié)點類型為3就進行修改操作,但也不行把整個節(jié)點的文本都修改,只需修改插值表達式的內(nèi)容,因此要使用正則表達式匹配,將匹配的結(jié)果保存到變量里,匹配的結(jié)果是一個數(shù)組,而索引為1的元素才是我們需要提取出來的元素,這個元素就是去除了{{}}和空格得到的字符串,然后就可以直接用Vue實例來訪問對應(yīng)屬性的值,修改完后return出去結(jié)束遞歸
function Compile(ele,vm){ vm.$el=document.querySelector(ele) //獲取元素保存在實例了的$el里 const fragment=document.createDocumentFragment() //創(chuàng)建文檔碎片 let child; while (child=vm.$el.firstChild){//循環(huán)將子節(jié)點添加到文檔碎片里 fragment.append(child) } fragment_compile(fragment) function fragment_compile(node){ //修改文本節(jié)點內(nèi)容 const pattern = /{{s*(S*)s*}}/ //檢索字符串中正則表達式的匹配,用于匹配插值表達式 if(node.nodeType===3){ const result = pattern.exec(node.nodeValue) if(result){ console.log('result[1]') const value=result[1].split('.').reduce(//split將對象里的屬性分布在數(shù)組里,鏈式地進行排列;reduce進行累加,層層遞進獲取$data的值 (total,current)=>total[current],vm.$data ) node.nodeValue=node.nodeValue.replace(pattern,value) //replace函數(shù)將插值表達式替換成$data里的屬性的值 } return } node.childNodes.forEach(child=>fragment_compile(child)) } vm.$el.appendChild(fragment) //將文檔碎片應(yīng)用到對應(yīng)的dom元素里面 }
頁面的內(nèi)容又出來了,插值表達式替換成了vm實例里的數(shù)據(jù)
訂閱發(fā)布者模式
雖然進行了數(shù)據(jù)劫持,和將數(shù)據(jù)應(yīng)用到頁面上,但是數(shù)據(jù)發(fā)生變動還不能及時更新,還需要實現(xiàn)訂閱發(fā)布者模式
首先創(chuàng)建一個類用來收集和通知訂閱者,生成實例的時候需要有一個數(shù)組存放訂閱者的信息,一個將訂閱者添加到這個數(shù)組里的方法和一個通知訂閱者的方法,調(diào)用這個方法就回去遍歷訂閱者的數(shù)組,讓訂閱者調(diào)用自身的update方法進行更新
class Dependency{ constructor(){ this.subscribers=[] //存放訂閱者的信息 } addSub(sub){ this.subscribers.push(sub) //將訂閱者添加到這個數(shù)組里 } notify(){ this.subscribers.forEach(sub=>sub.update()) //遍歷訂閱者的數(shù)組,調(diào)用自身的update函數(shù)進行更新 } }
設(shè)置訂閱者類,需要用到Vue實例上的屬性,需要Vue實例和Vue實例對應(yīng)的屬性和一個回調(diào)函數(shù)作為參數(shù),將參數(shù)都賦值給實例
然后就可以創(chuàng)建訂閱者的update函數(shù),在函數(shù)里調(diào)用傳進來的回調(diào)函數(shù)
class Watcher{ constructor(vm,key,callback){//將參數(shù)都賦值給Watcher實例 this.vm=vm this.key=key this.callback=callback } update(){ this.callback() } }
替換文檔碎片內(nèi)容的時候需要告訴訂閱者如何更新,所以訂閱者實例在模板解析把節(jié)點值替換內(nèi)容的時候創(chuàng)建,傳入vm實例,exec匹配成功后的索引值1和回調(diào)函數(shù),將替換文本的執(zhí)行語句復(fù)制到回調(diào)函數(shù)里,通知訂閱者更新的時候就調(diào)用這個回調(diào)函數(shù)
回調(diào)函數(shù)里的nodeValue要提前保存,不然替換的內(nèi)容就不是插值表達式而是替換過的內(nèi)容
然后就要想辦法將訂閱者存儲到Dependency實例的數(shù)組里,我們可以在構(gòu)造Watcher實例的時候保存實例到訂閱者數(shù)組里
Dependency.temp=this //設(shè)置一個臨時屬性temp
將新的訂閱者添加到訂閱者數(shù)組里且還要將所有的訂閱者都進行同樣的操作,那么就可以在觸發(fā)get的時候?qū)⒂嗛喺咛砑拥接嗛喺邤?shù)組里,為了正確觸發(fā)對應(yīng)的屬性get,需要用reduce方法對key進行同樣的操作
可以看到控制臺打印出了Wathcer實例,每個實例都不同,都對應(yīng)不同的屬性值
Dependency類還沒創(chuàng)建實例,里面的訂閱者數(shù)組是不存在的,所以要先創(chuàng)建實例再將訂閱者添加到訂閱者數(shù)組里
修改數(shù)據(jù)的時候通知訂閱者來進行更新,在set里調(diào)用dependency的通知方法,通知方法就會去遍數(shù)組,訂閱者執(zhí)行自己的update方法進行數(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) }
在控制臺修改屬性值都修改成功了,頁面也自動更新了
完成了文本的綁定就可以綁定輸入框了,在vue里通過v-model進行綁定,因此要判斷哪個節(jié)點有v-model,元素節(jié)點的類型是1,可以使用nodeName來匹配input元素,直接在判斷文本節(jié)點下面進行新的判斷
if(node.nodeType===1&&node.nodeName==='INPUT'){ const attr=Array.from(node.attributes) console.log(attr); }
節(jié)點名字nodeName為v-model,nodeValue為name,就是數(shù)據(jù)里的屬性名
因此對這個數(shù)組進行遍歷,匹配到了v-model根據(jù)nodeValue找到對應(yīng)的屬性值,把屬性值賦值到節(jié)點上,同時為了在數(shù)據(jù)更新后訂閱者知道更新自己,也要在INPUT節(jié)點里新增Watcher實例
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 }) } })
修改屬性值,頁面也作出修改
最后剩下用視圖改變數(shù)據(jù),在v-model的節(jié)點上使用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 })
(學習視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)