vue實現簡易的雙向資料繫結
阿新 • • 發佈:2020-12-30
主要是通過資料劫持和釋出訂閱一起實現的
- 雙向資料繫結 資料更新時,可以更新檢視 檢視的資料更新是,可以反向更新模型
組成說明
- 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 實現雙向資料繫結的資料請關注我們其它相關文章!