研究了一下vue雙向綁定的原理,所以簡單記錄一下,以下例子只是簡單實現,還請大家不要吐槽~
之前也了解過vue是通過數據劫持+訂閱發布模式來實現MVVM的雙向綁定的,但一直沒仔細研究,這次深入學習了一下,借此機會分享給大家。
首先先將流程圖給大家看一下

參考文章:Vue.js雙向綁定的實現原理
我雖然參考的是這篇文章,下面的代碼也是在閱讀幾遍后仿造的,自己只是簡單添加了個遞歸實現所有dom子節點的雙向綁定,以及添加了一些理解,但真正讓我了然于心,讓我可以獨立寫出2遍完整邏輯的其實是這張圖,所以個人認為這張流程圖才是最重要的,而我參考的這篇文章的作者也是參考這幅圖的原作者的。
站在閱讀和理解MVVM的完整邏輯的話,推薦大家看第一篇,但是第二篇原文章的圖文更能說明一些問題
如果大家看了我的解釋也能夠完全理解的話,那就更好啦啦啦啦啦~哈哈
好,下面我會從2個角度開始講解,先上單向綁定,再由單向綁定過渡到雙向綁定;
首先,先為大家解釋一下單向綁定model => view層的邏輯
1、劫持dom結構;
2、創建文檔碎片,利用文檔碎片重構dom結構;
3、在重構的過程中解析dom結構實現MVVM構造函數實例化后的數據初始化視圖數據;
4、利用判斷dom一級子元素是否依然有子元素從而進行所有子元素的單向綁定;
5、將文檔碎片添加至根節點中.
這就是我總結的關于單向綁定的邏輯了,下面利用代碼跟大家解釋
//dom結構<div id="app"> <input type="text" v-model="msg"> <p>{{msg}}</p> <ul> <li>1</li> <li>{{msg}}</li> <li>{{test}}</li> </ul></div>//one-way-binding.js //判斷每個dom節點是否擁有子節點,若有則返回該節點 function isChild(node){ //這里使用childNodes可以讀取text文本節點,所以不用children if(node.childNodes.length ===0){ return false; } else{ return node; } } //利用文檔碎片劫持dom結構及數據,進而進行dom的重構 function nodeToFragment(node,vm){ var frag = document.createDocumentFragment(); var child; while(child = node.firstChild){ //一級dom節點數據綁定 compile(child,vm); //判斷每個一級dom節點是否有二級節點,若有則遞歸處理文檔碎片 if(isChild(child)){ //遞歸實現二級dom節點的重構 nodeToFragment(isChild(child),vm); } frag.appendChild(child); } //將文檔碎片添加至對應node中,最后為id為app的元素下 node.appendChild(frag); } //初始化綁定數據 function compile(node,vm){ //node節點為元素節點時 if(node.nodeType === 1){ var attr = node.attributes; //遍歷當前節點的所有屬性 for(var i=0;i<attr.length;i++){ if(attr[i].nodeName === 'v-model'){ //屬性名 var name = attr[i].nodeValue; //將data下對應屬性名的值賦值給當前節點值 //這里因為node是input標簽所以值為node.value node.value = vm.data[name]; //最后標簽中的v-model屬性也可以功成身退了,刪除它 node.removeAttribute(attr[i].nodeName); } } } //node節點為text文本節點#text時 if(node.nodeType === 3){ var reg = //{/{(.*)/}/}/; if(reg.test(node.nodeValue.trim())){ //將正則匹配到的{{}}中的字符串賦值給name var name = RegExp.$1; //利用name對應賦值相應的節點值 node.nodeValue = vm.data[name]; } } } //MVVM構造函數,這里我就寫成Vue了 function Vue(options){ this.id = options.el; this.data = options.data; //將根節點與實例化后的對象作為參數傳入 nodeToFragment(document.getElementById(this.id),this); } //實例化 var vm = new Vue({ el:'app', data:{ msg:'hello,two-ways-binding', test:'test key' } })上述就是簡單的單向綁定了,整個邏輯實際上非常簡單,我再來跟大家說明一下
1、為了令model層的數據可以綁定到view層的dom上,所以我們想了一個辦法來替換dom中的一些元素值,而明顯一個個替換時不可取的,因為大量的dom操作會降低程序的運行效率,你想想,每次dom操作可都是一次對dom整體的遍歷過程~,所以我們覺得采用文檔碎片的形式,將dom一次全部劫持,在內存中執行全部數據綁定操作,最后只進行一次dom操作,即添加子節點來解決這個頻繁操作dom的問題,你也可以理解為中間的一層存在于內存中的虛擬dom;
2、那么既然如此,我們就要首先劫持所有dom節點,這里我們利用nodeToFragment函數來劫持;
3、在每次劫持對應dom節點的過程中,我們也會相對應的實現對該dom元素的數據綁定,以求在最后直接添加到為根節點的子元素即可,這個過程我們就在nodeToFragment函數中插入了compile函數來初始化綁定,并且添加遞歸函數實現所有子元素的初始綁定;
4、在compile函數中我們添加的數據又從何而來呢?對,正是因為這點,所以我們建立MVVM的構造函數Vue來實現數據支持,并實現在實例化時就執行nodeToFragment同時重構dom和實現初始化綁定compile;
5、好了,單向綁定就是這么簡單,4個函數即可Vue => nodeToFragment => compile => isChild。
完成圖如下

好了,再回過來看看整體的流程圖,我們已經實現了這一塊了

接下來,休息下,大家準備開始流程圖后面的雙向綁定,ok,還是按照單向綁定的順序,先跟大家講明實現邏輯;
1、創建數據監聽者observer去監聽view層數據的變化;(利用Object.defineProperty劫持所有要用到的數據)
2、當view層數據變化后,通過通知者Dep通知訂閱者去實現數據的更新;(通知后,遍歷所有用到數據的訂閱者更新數據)
3、訂閱者watcher接收到view層數據變更后,重新對變化的數據進行賦值,改變model層,從而改變所有view層用到過該數據的地方。(更新數據,并改變view層所有用到該數據的節點值)
上面是實現邏輯,下面將通過具體代碼告訴大家每一步的做法,由于雙向綁定中訂閱者會涉及初始化綁定的過程,所以代碼量較多,我會在大更改處用――為大家框出來
//判斷每個dom節點是否擁有子節點,若有則返回該節點 function isChild(node){ if(node.childNodes.length ===0){ return false; } else{ return node; } } //利用文檔碎片劫持dom結構及數據,進而進行dom的重構 function nodeToFragment(node,vm){ var frag = document.createDocumentFragment(); var child; while(child = node.firstChild){ //一級dom節點數據綁定 compile(child,vm); //判斷每個一級dom節點是否有二級節點,若有則遞歸處理文檔碎片 if(isChild(child)){ nodeToFragment(isChild(child),vm); } frag.appendChild(child); } node.appendChild(frag); } //初始化綁定數據 function compile(node,vm){ //node節點為元素節點時 if(node.nodeType === 1){ var attr = node.attributes; for(var i=0;i<attr.length;i++){ if(attr[i].nodeName === 'v-model'){ var name = attr[i].nodeValue; //特殊處理input標簽 //------------------------ if(node.nodeName === 'INPUT'){ node.addEventListener('keyup',function(e){ vm[name] = e.target.value; }) } //由于數據已經由data劫持至vm下,所以直接賦值vm[name]即可觸發getter訪問器 node.value = vm[name]; //------------------------- node.removeAttribute(attr[i].nodeName); } } } //node節點為text文本節點時 if(node.nodeType === 3){ var reg = //{/{(.*)/}/}/; if(reg.test(node.nodeValue.trim())){ var name = RegExp.$1; //node.nodeValue = vm[name]; //---------------------- //為每個節點建立訂閱者,通過訂閱者watcher初始化及更新視圖數據 new watcher(vm,node,name); //----------------------- } } } //---------------------------------------------------------------- //訂閱者(為每個節點的數據建立watcher隊列,每次接受更改數據需求后,利用劫持數據執行對應節點的數據更新) function watcher(vm,node,name){ //將每個掛載了數據的dom節點添加到通知者列表,要保證每次創建watcher時只有一個添加目標,否則后續會因為watcher是全局而被覆蓋,所以每次要清空目標 Dep.target = this; this.vm = vm; this.node = node; this.name = name; //執行update的時候會調用監聽者劫持的getter事件,從而添加到watcher隊列,因為update中有訪問this.vm[this.name] this.update(); //為保證只有一個全局watcher,添加到隊列后,清空全局watcher Dep.target = null; } watcher.prototype = { update(){ this.get(); //input標簽特殊處理化 if(this.node.nodeName === 'INPUT'){ this.node.value = this.value; } else{ this.node.nodeValue = this.value; } }, get(){ //這里調用了數據劫持的getter this.value = this.vm[this.name]; } }; //通知者(將監聽者的更改信息需求發送給訂閱者,告訴訂閱者哪些數據需要更改) function Dep(){ this.subs = []; } Dep.prototype = { addSub(watcher){ //添加用到數據的節點進入watcher隊列 this.subs.push(watcher); }, notify(){ //遍歷watcher隊列,令相應數據節點重新更新view層數據,model => view this.subs.forEach(function(watcher){ watcher.update(); }) } }; //監聽者(利用setter監聽view => model的數據變化,發出通知更改model數據后再從model => view更新視圖所有用到該數據的地方) function observer(data,vm){ //遍歷劫持data下所有屬性 Object.keys(data).forEach(function(key){ defineReactive(vm,key,data[key]); }) } function defineReactive(vm,key,val){ //新建通知者 var dep = new Dep(); //靈活利用setter與getter訪問器 Object.defineProperty(vm,key,{ get(){ //初始化數據更新時將每個數據的watcher添加至隊列棧中 if(Dep.target) dep.addSub(Dep.target); return val; }, set(newVal){ if(val === newVal) return ; //初始化后,文檔碎片中的虛擬dom已與model層數據綁定起來了 val = newVal; //同步更新model中data屬性下的數據 vm.data[key] = val; //數據有改動時向通知者發送通知 dep.notify(); } }) } //--------------------------------------------------------------- function Vue(options){ this.id = options.el; this.data = options.data; observer(this.data,this); nodeToFragment(document.getElementById(this.id),this); } var vm = new Vue({ el:'app', data:{ msg:'hello,two-ways-binding', test:'test key' } })好的,到這里雙向綁定的講解也就結束了,代碼量確實有點多,但是我們看到其實邏輯在你熟悉后并不復雜,特別是參照了上文的流程圖后,其實就是:
1、通過observer劫持所有model層數據到vue下,并在劫持時靈活運用getter與setter訪問器屬性來在虛擬dom初始化數據綁定時,利用此時的get方法綁定初始化數據進入通知者隊列,后續初始化完成后,在view層數據發生變化時,利用set方法及時利用通知者發出通知;
2、在dep通知者接收到有一處dom節點數據更改的通知時,遍歷watcher隊列及告訴watcher訂閱者,view層數據有所變動model層已經相應改變,你要重新執行update將model層的數據更新到view層所有用到該數據的地方(比如我們利用input實現的雙向綁定就不止一個dom節點內使用了,而是多個,所以必須整體遍歷修改)。
3、這是一個model => view => model =>view的過程,真正的邏輯順序為model => view,view => model,model => view,反復的過程~
貼上結果圖
初始未改動view層數據圖

修改view層數據后圖

最后大家再看一次流程圖,看看整個邏輯是不是跟流程圖一模一樣,通過流程圖就可以回憶起這個邏輯過程,寫多2遍就可以記住!

以上只是通過簡單實現來告訴大家vue的數據劫持+訂閱發布模式這個雙向綁定的原理,其中有很多細節上的不足可能未作處理,還請見諒~
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答