1. 程式人生 > 程式設計 >vue實現簡易的雙向資料繫結

vue實現簡易的雙向資料繫結

主要是通過資料劫持和釋出訂閱一起實現的

  • 雙向資料繫結 資料更新時,可以更新檢視 檢視的資料更新是,可以反向更新模型

組成說明

  • Observe監聽器 劫持資料,感知資料變化,發出通知給訂閱者,在get中將訂閱者新增到訂閱器中
  • Dep訊息訂閱器 儲存訂閱者,通知訂閱者呼叫更新函式
  • 訂閱者Wather取出模型值,更新檢視
  • 解析器Compile 解析指令,更新模板資料,初始化檢視,例項化一個訂閱者,將更新函式繫結到訂閱者上,可以在接收通知二次更新檢視,對於v-model還需要監聽input事件,實現檢視到模型的資料流動

基本結構

HTML模板

<divid="app">
<form>
<inputtype="text"v-model="username">
</form>
<pv-bind="username"></p>
</div>
  • 一個根節點#app
  • 表單元素,裡面包含input,使用v-model指令繫結資料username
  • p元素上使用v-bind繫結數username

MyVue類

簡單的模擬Vue類

將例項化時的選項options,資料options.data進行儲存 此外,通過options.el獲取dom元素,儲存到$el上

classMyVue{
constructor(options){
this.$options=options
this.$el=document.querySelector(this.$options.el)
this.$data=options.data
}
}

例項化MyVue

例項化一個MyVue,傳遞選項進去,選項中指定繫結的元素el和資料物件data

constmyVm=newMyVue({
el:'#app',data:{
username:'LastStarDust'
}
})

Observe監聽器實現

劫持資料是為了修改資料的時候可以感知,發出通知,執行更新檢視操作

classMyVue{
constructor(options){
// ...
//監視資料的屬性
this.observable(this.$data)
}
//遞迴遍歷資料物件的所有屬性,進行資料屬性的劫持{username:'LastStarDust'}
observable(obj){
//obj為空或者不是物件,不做任何操作
constisEmpty=!obj||typeofobj!=='object'
if(isEmpty){
return
}

//['username']
constkeys=Object.keys(obj)
keys.forEach(key=>{
//如果屬性值是物件,遞迴呼叫
letval=obj[key]
if(typeofval==='object'){
this.observable(val)
}
//this.defineReactive(this.$data,'username','LastStarDust')
this.defineReactive(obj,key,val)
})

returnobj
}

//資料劫持,修改屬性的get和set方法
defineReactive(obj,val){
Object.defineProperty(obj,{
enumerable:true,configurable:true,get(){
console.log(`取出${key}屬性值:值為${val}`)
returnval
},set(newVal){
//沒有發生變化,不做更新
if(newVal===val){
return
}
console.log(`更新屬性${key}的值為:${newVal}`)
val=newVal
}
})
}
}

Dep訊息訂閱器

儲存訂閱者,收到通知時,取出訂閱者,呼叫訂閱者的update方法

//定義訊息訂閱器
classDep{
//靜態屬性Dep.target,這是一個全域性唯一的Watcher,因為在同一時間只能有一個全域性的Watcher
statictarget=null
constructor(){
//儲存訂閱者
this.subs=[]
}
//新增訂閱者
add(sub){
this.subs.push(sub)
}
//通知
notify(){
this.subs.forEach(sub=>{
//呼叫訂閱者的update方法
sub.update()
})
}
}

將訊息訂閱器新增到資料劫持過程中

為每一個屬性新增訂閱者

defineReactive(obj,val){
constdep=newDep()
Object.defineProperty(obj,get(){
//會在初始化時,觸發屬性get()方法,來到這裡Dep.target有值,將其作為訂閱者儲存起來,在觸發屬性的set()方法時,呼叫notify方法
if(Dep.target){
dep.add(Dep.target)
}
console.log(`取出${key}屬性值:值為${val}`)
returnval
},不做更新
if(newVal===val){
return
}
console.log(`更新屬性${key}的值為:${newVal}`)

val=newVal
dep.notify()
}
})
}

訂閱者Wather

從模型中取出資料並更新檢視

//定義訂閱者類
classWather{
constructor(vm,exp,cb){
this.vm=vm//vm例項
this.exp=exp//指令對應的字串值,如v-model="username",exp相當於"username"
this.cb=cb//回到函式更新檢視時呼叫
this.value=this.get()//將自己新增到訊息訂閱器Dep中
}

get(){
//將當前訂閱者作為全域性唯一的Wather,新增到Dep.target上
Dep.target=this
//獲取資料,觸發屬性的getter方法
constvalue=this.vm.$data[this.exp]
//在執行新增到訊息訂閱Dep後,重置Dep.target
Dep.target=null
returnvalue
}

//執行更新
update(){
this.run()
}

run(){
//從Model模型中取出屬性值
constnewVal=this.vm.$data[this.exp]
constoldVal=this.value
if(newVal===oldVal){
returnfalse
}
//執行回撥函式,將vm例項,新值,舊值傳遞過去
this.cb.call(this.vm,newVal,oldVal)
}
}

解析器Compile

  • 解析模板指令,並替換模板資料,初始化檢視;
  • 將模板指令對應的節點繫結對應的更新函式,初始化相應的訂閱器;
  • 初始化編譯器,儲存el對應的dom元素,儲存vm例項,呼叫初始化方法
  • 在初始化方法中,從根節點開始,取出根節點的所有子節點,逐個對節點進行解析
  • 解析節點過程中
  • 解析指令存在,取出繫結值,替換模板資料,完成首次檢視的初始化
  • 給指令對應的節點繫結更新函式,並例項化一個訂閱器Wather
  • 對於v-model指令,監聽'input'事件,實現檢視更新是,去更新模型的資料
//定義解析器
//解析指令,初始檢視
//模板的指令繫結更新函式,資料更新時,更新檢視
classCompile{
constructor(el,vm){
this.el=el
this.vm=vm
this.init(this.el)
}

init(el){
this.compileEle(el)
}
compileEle(ele){
constnodes=ele.children
		// 遍歷節點進行解析
for(constnodeofnodes){
		 // 如果有子節點,遞迴呼叫
if(node.children&&node.children.length!==0){
this.compileEle(node)
}

//指令時v-model並且是標籤是輸入標籤
consthasVmodel=node.hasAttribute('v-model')
constisInputTag=['INPUT','TEXTAREA'].indexOf(node.tagName)!==-1
if(hasVmodel&&isInputTag){
constexp=node.getAttribute('v-model')
constval=this.vm.$data[exp]
constattr='value'
//初次模型值推到檢視層,初始化檢視
this.modelToView(node,val,attr)
//例項化一個訂閱者,未來資料更新,可以更新檢視
newWather(this.vm,(newVal)=>{
this.modelToView(node,attr)
})

//監聽檢視的改變
node.addEventListener('input',(e)=>{
this.viewToModel(exp,e.target.value)
})
}
		 
		 // 指令時v-bind
if(node.hasAttribute('v-bind')){
constexp=node.getAttribute('v-bind')
constval=this.vm.$data[exp]
constattr='innerHTML'
//初次模型值推到檢視層,attr)
})
}
}
}
//將模型值更新到檢視
modelToView(node,attr){
node[attr]=val
}
//將檢視值更新到模型上
viewToModel(exp,val){
this.vm.$data[exp]=val
}
}

完整程式碼

<!DOCTYPEhtml>
<htmllang="en">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<divid="app">
<form>
<inputtype="text"v-model="username">
</form>
<div>
<spanv-bind="username"></span>
</div>
<pv-bind="username"></p>
</div>
<script>

classMyVue{
constructor(options){
this.$options=options
this.$el=document.querySelector(this.$options.el)
this.$data=options.data

//監視資料的屬性
this.observable(this.$data)

//編譯節點
newCompile(this.$el,this)
}
//遞迴遍歷資料物件的所有屬性,不做更新
if(newVal===val){
return
}
console.log(`更新屬性${key}的值為:${newVal}`)

val=newVal
dep.notify()
}
})
}
}

//定義訊息訂閱器
classDep{
//靜態屬性Dep.target,這是一個全域性唯一的Watcher,因為在同一時間只能有一個全域性的Watcher
statictarget=null
constructor(){
//儲存訂閱者
this.subs=[]
}
//新增訂閱者
add(sub){
this.subs.push(sub)
}
//通知
notify(){
this.subs.forEach(sub=>{
//呼叫訂閱者的update方法
sub.update()
})
}
}

//定義訂閱者類
classWather{
constructor(vm,oldVal)
}
}

//定義解析器
//解析指令,vm){
this.el=el
this.vm=vm
this.init(this.el)
}

init(el){
this.compileEle(el)
}
compileEle(ele){
constnodes=ele.children
for(constnodeofnodes){
if(node.children&&node.children.length!==0){
//遞迴呼叫,編譯子節點
this.compileEle(node)
}

//指令時v-model並且是標籤是輸入標籤
consthasVmodel=node.hasAttribute('v-model')
constisInputTag=['INPUT',e.target.value)
})
}

if(node.hasAttribute('v-bind')){
constexp=node.getAttribute('v-bind')
constval=this.vm.$data[exp]
constattr='innerHTML'
//初次模型值推到檢視層,val){
this.vm.$data[exp]=val
}
}

constmyVm=newMyVue({
el:'#app',data:{
username:'LastStarDust'
}
})

//console.log(Dep.target)
</script>
</body>
</html>

以上就是vue實現簡易的雙向資料繫結的詳細內容,更多關於vue 實現雙向資料繫結的資料請關注我們其它相關文章!