雙向綁定是指既可以數據驅動視圖,又可以視圖驅動數據,那么要實現這樣一個功能意味着我們需要將dom節點中與雙向綁定相關的指令與屬性傳入一個類似於“加工工廠”的類中,進行篩選和加工,最后實現一個甄別出關鍵參數和指令並根據指令實現數據雙向綁定的完整“工藝流程”。
vue數據響應式雙向綁定的實現機制,大致如下流程:
實現具體思路如下:我們new出的MVVM類內含有各種屬性,我們就需要實現一個監聽類Observer,內含Object.defineProperty()方法去劫持各個屬性的setter和getter以時刻察覺到MVVM類的變化,然后再將這個變化通知給Dep訂閱器,Dep訂閱器算是一個通知觀察者的通信員和儲存不同觀察着不同屬性的觀察者(每個觀察者只能觀察一個屬性)的容器,同時Dep訂閱器通知觀察者Watcher去更新DOM節點從而實現視圖的更新。流程如下圖紅線:
接下來實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,而在初始化視圖時,走的時下圖的紅線流程,當解析器識別到數據變動時,又會走藍線的流程去更新視圖。
我們將實現該圖流程划分如下:
1. 實現一個指令解析器Compile
2. 實現一個數據監聽器Observer
3. 實現一個觀察者Watcher(來更新視圖)
4. 實現一個proxy代理
1.實現一個指令解析器Compile
Compile對el綁定的根元素的子節點的指令(如v-text,{{}},v-on等)進行一個掃描和解析,根據指令模板找到對應的掛載節點去替換數據,具體實現的方式就是綁定一個回調更新函數,傳遞給Watcher來更新視圖。此外還要在Compile中獲取一個文檔碎片對象,放入內存中會減少頁面的回流和重繪,文檔碎片對象中對元素對象和文本對象進行處理,並甄別出傳入的text,model等不同指令並針對不同的指令處理不同的事件。
1 class Compile{ 2 constructor(el,vm){ 3 // 判斷el是否為元素節點,不是則獲取其值 4 this.el=this.isElementNode(el) ? el : document.querySelector(el); 5 this.vm = vm; 6 // 1.獲取文檔碎片對象 放入內存中減少頁面的回流重繪 7 const fragment = this.node2Fragment(this.el); 8 // console.log(fragment); 9 // 2.編譯fragment模板 10 this.compile(fragment); 11 // 3.追加子元素到根元素 12 this.el.appendChild(fragment); 13 }; 14 15 // 編譯fragment模板的方法 16 compile(fragment){ 17 // 1.獲取到每一個子節點 18 const childNodes=fragment.childNodes; 19 [...childNodes].forEach(child=>{ 20 if(this.isElementNode(child)){ 21 // 是元素節點 22 // 編譯元素節點 23 this.compileElement(child); 24 }else{ 25 // 文本節點 26 // 是否編譯文本節點 27 this.compileText(child); 28 } 29 30 if(child.childNodes && child.childNodes.length){ 31 this.compile(child); 32 } 33 34 }) 35 }; 36 37 // 編譯元素節點的方法 38 compileElement(node){ 39 const attributes=node.attributes; 40 [...attributes].forEach(attr=>{ 41 const {name,value}=attr; 42 if(this.isDirective(name)){ //判斷是否為一個指令 43 const [ ,dirctive]=name.split('-'); 44 // console.log(dirctive); 45 const[dirName,eventName] = dirctive.split(':'); 46 // 更新數據,數據驅動視圖 47 compileUtil[dirName](node,value,this.vm,eventName); 48 // 刪除有指令的標簽上的屬性 49 node.removeAttribute('v-' + dirctive); 50 }else if(this.isEventName(name)){ 51 let[ ,eventName]= name.split('@'); 52 compileUtil['on'](node,value,this.vm,eventName); 53 } 54 }) 55 } 56 57 // 是否編譯文本節點的方法 58 compileText(node){ 59 // 編譯{{}} 60 const content =node.textContent; 61 // console.log(content); 62 if(/\{\{(.+?)\}\}/.test(content)){ 63 compileUtil['text'](node,content,this.vm) 64 } 65 } 66 67 isEventName(attrName){ 68 return attrName.startsWith('@') 69 } 70 71 isDirective(attrName){ 72 return attrName.startsWith('v-') 73 } 74 75 // 獲取所有孩子節點 76 node2Fragment(el){ 77 // 1.創建文檔碎片 78 const f=document.createDocumentFragment(); 79 let firstChild; 80 // 2.遍歷取出所有節點加入f中 81 while(firstChild=el.firstChild){ 82 f.appendChild(firstChild); 83 } 84 return f; 85 } 86 87 // 判斷是否為元素節點的方法 88 isElementNode(node){ 89 return node.nodeType === 1; 90 } 91 }
2.實現一個數據監聽器Observer
我們接下來實現的是一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值通知依賴收集對象(Dep)並通知訂閱者(Watcher)來更新視圖。
1 class Observer{ 2 constructor(data){ 3 this.observe(data) 4 } 5 6 // 觀察數據的方法 7 observe(data){ 8 if(data && typeof data==='object'){ 9 //console.log(Object.keys(data)); 10 Object.keys(data).forEach(key=>{ 11 this.defineReactive(data,key,data[key]) 12 }) 13 } 14 } 15 16 defineReactive(obj,key,value){ 17 //遞歸遍歷 18 this.observe(value); 19 const dep=new Dep(); 20 // 劫持並監聽所有的屬性 21 Object.defineProperty(obj,key,{ 22 enumerable:true, 23 configurable:false, 24 get(){ 25 // 訂閱數據變化時,往Dep中添加觀察者 26 Dep.target && dep.addSub(Dep.target) 27 return value; 28 }, 29 set:(newVal)=>{ 30 this.observe(newVal) 31 if(newVal!==value){ 32 value=newVal; 33 } 34 // 告訴dep通知變化 35 dep.notify() 36 } 37 }) 38 } 39 }
3. 實現一個Watcher
它作為連接Observer和Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖
只要所做事情:
1、在自身實例化時往屬性訂閱器(dep)里面添加自己
2、自身必須有一個update()方法
3、待屬性變動dep.notify()通知時,能調用自身的update()方法,並觸發Compile中綁定的回調。
1 class Watcher{ 2 constructor(vm,expr,cb){ 3 this.vm=vm; 4 this.expr=expr; 5 this.cb=cb; 6 // 先把舊值保存起來 7 this.oldVal = this.getOldVal() 8 } 9 10 getOldVal(){ 11 Dep.target=this; 12 const oldVal= compileUtil.getVal(this.expr,this.vm); 13 Dep.target=null; 14 return oldVal; 15 } 16 17 update(){ 18 const newVal= compileUtil.getVal(this.expr,this.vm); 19 if(newVal!==this.oldVal){ 20 this.cb(newVal); 21 } 22 } 23 } 24 25 // 實現一個訂閱器 26 class Dep{ 27 constructor(){ 28 this.subs=[]; 29 } 30 // 收集觀察者 31 addSub(watcher){ 32 this.subs.push(watcher); 33 } 34 // 通知觀察者去更新 35 notify(){ 36 this.subs.forEach(w=>w.update()) 37 } 38 }
4. 實現一個proxy代理
我們在使用vue的時候,通常可以直接vm.msg來獲取數據,這是因為vue源碼內部做了一層代理.也就是說把數據獲取操作vm上的取值操作 都代理到vm.$data上
1 proxyData(data){ 2 for(const key in data){ 3 Object.defineProperty(this,key,{ 4 get(){ 5 return data[key]; 6 }, 7 set(newVal){ 8 data[key]=newVal; 9 } 10 }) 11 } 12 }
致此一個簡易的vue源碼就實現了,我們來測試一下:
1 <script src="./Observer.js"></script> 2 <script src="./Myvue.js"></script> 3 <script> 4 let vm=new Myvue({ 5 el:'#app', 6 data:{ 7 person:{ 8 name:"funkyou", 9 age:21, 10 fav:'ukulele', 11 }, 12 msg:"mvvm自制版", 13 htmlStr:'<h3>我是一個前端實習生</h3>' 14 }, 15 methods:{ 16 handleClick(){ 17 // console.log(this); 18 this.$data.person.name='Chrome' 19 } 20 }, 21 }) 22 </script>
對應的頁面如圖:
實現了數據的改變同步到頁面上。
在我的GitHub上查看源碼:
https://github.com/173392531/vue-