vue雙向綁定的簡(jiǎn)單實(shí)現(xiàn)
研究了一下vue雙向綁定的原理,所以簡(jiǎn)單記錄一下,以下例子只是簡(jiǎn)單實(shí)現(xiàn),還請(qǐng)大家不要吐槽~
之前也了解過vue是通過數(shù)據(jù)劫持+訂閱發(fā)布模式來實(shí)現(xiàn)MVVM的雙向綁定的,但一直沒仔細(xì)研究,這次深入學(xué)習(xí)了一下,借此機(jī)會(huì)分享給大家。
首先先將流程圖給大家看一下
參考文章:Vue.js雙向綁定的實(shí)現(xiàn)原理
我雖然參考的是這篇文章,下面的代碼也是在閱讀幾遍后仿造的,自己只是簡(jiǎn)單添加了個(gè)遞歸實(shí)現(xiàn)所有dom子節(jié)點(diǎn)的雙向綁定,以及添加了一些理解,但真正讓我了然于心,讓我可以獨(dú)立寫出2遍完整邏輯的其實(shí)是這張圖,所以個(gè)人認(rèn)為這張流程圖才是最重要的,而我參考的這篇文章的作者也是參考這幅圖的原作者的。
原文章:剖析Vue原理&實(shí)現(xiàn)雙向綁定MVVM
站在閱讀和理解MVVM的完整邏輯的話,推薦大家看第一篇,但是第二篇原文章的圖文更能說明一些問題
如果大家看了我的解釋也能夠完全理解的話,那就更好啦啦啦啦啦~哈哈
好,下面我會(huì)從2個(gè)角度開始講解,先上單向綁定,再由單向綁定過渡到雙向綁定;
首先,先為大家解釋一下單向綁定model => view層的邏輯
1、劫持dom結(jié)構(gòu);
2、創(chuàng)建文檔碎片,利用文檔碎片重構(gòu)dom結(jié)構(gòu);
3、在重構(gòu)的過程中解析dom結(jié)構(gòu)實(shí)現(xiàn)MVVM構(gòu)造函數(shù)實(shí)例化后的數(shù)據(jù)初始化視圖數(shù)據(jù);
4、利用判斷dom一級(jí)子元素是否依然有子元素從而進(jìn)行所有子元素的單向綁定;
5、將文檔碎片添加至根節(jié)點(diǎn)中.
這就是我總結(jié)的關(guān)于單向綁定的邏輯了,下面利用代碼跟大家解釋
//dom結(jié)構(gòu) <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 //判斷每個(gè)dom節(jié)點(diǎn)是否擁有子節(jié)點(diǎn),若有則返回該節(jié)點(diǎn) function isChild(node){ //這里使用childNodes可以讀取text文本節(jié)點(diǎn),所以不用children if(node.childNodes.length ===0){ return false; } else{ return node; } } //利用文檔碎片劫持dom結(jié)構(gòu)及數(shù)據(jù),進(jìn)而進(jìn)行dom的重構(gòu) function nodeToFragment(node,vm){ var frag = document.createDocumentFragment(); var child; while(child = node.firstChild){ //一級(jí)dom節(jié)點(diǎn)數(shù)據(jù)綁定 compile(child,vm); //判斷每個(gè)一級(jí)dom節(jié)點(diǎn)是否有二級(jí)節(jié)點(diǎn),若有則遞歸處理文檔碎片 if(isChild(child)){ //遞歸實(shí)現(xiàn)二級(jí)dom節(jié)點(diǎn)的重構(gòu) nodeToFragment(isChild(child),vm); } frag.appendChild(child); } //將文檔碎片添加至對(duì)應(yīng)node中,最后為id為app的元素下 node.appendChild(frag); } //初始化綁定數(shù)據(jù) function compile(node,vm){ //node節(jié)點(diǎn)為元素節(jié)點(diǎn)時(shí) if(node.nodeType === 1){ var attr = node.attributes; //遍歷當(dāng)前節(jié)點(diǎn)的所有屬性 for(var i=0;i<attr.length;i++){ if(attr[i].nodeName === 'v-model'){ //屬性名 var name = attr[i].nodeValue; //將data下對(duì)應(yīng)屬性名的值賦值給當(dāng)前節(jié)點(diǎn)值 //這里因?yàn)閚ode是input標(biāo)簽所以值為node.value node.value = vm.data[name]; //最后標(biāo)簽中的v-model屬性也可以功成身退了,刪除它 node.removeAttribute(attr[i].nodeName); } } } //node節(jié)點(diǎn)為text文本節(jié)點(diǎn)#text時(shí) if(node.nodeType === 3){ var reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue.trim())){ //將正則匹配到的{{}}中的字符串賦值給name var name = RegExp.$1; //利用name對(duì)應(yīng)賦值相應(yīng)的節(jié)點(diǎn)值 node.nodeValue = vm.data[name]; } } } //MVVM構(gòu)造函數(shù),這里我就寫成Vue了 function Vue(options){ this.id = options.el; this.data = options.data; //將根節(jié)點(diǎn)與實(shí)例化后的對(duì)象作為參數(shù)傳入 nodeToFragment(document.getElementById(this.id),this); } //實(shí)例化 var vm = new Vue({ el:'app', data:{ msg:'hello,two-ways-binding', test:'test key' } })
上述就是簡(jiǎn)單的單向綁定了,整個(gè)邏輯實(shí)際上非常簡(jiǎn)單,我再來跟大家說明一下
1、為了令model層的數(shù)據(jù)可以綁定到view層的dom上,所以我們想了一個(gè)辦法來替換dom中的一些元素值,而明顯一個(gè)個(gè)替換時(shí)不可取的,因?yàn)榇罅康膁om操作會(huì)降低程序的運(yùn)行效率,你想想,每次dom操作可都是一次對(duì)dom整體的遍歷過程~,所以我們覺得采用文檔碎片的形式,將dom一次全部劫持,在內(nèi)存中執(zhí)行全部數(shù)據(jù)綁定操作,最后只進(jìn)行一次dom操作,即添加子節(jié)點(diǎn)來解決這個(gè)頻繁操作dom的問題,你也可以理解為中間的一層存在于內(nèi)存中的虛擬dom;
2、那么既然如此,我們就要首先劫持所有dom節(jié)點(diǎn),這里我們利用nodeToFragment函數(shù)來劫持;
3、在每次劫持對(duì)應(yīng)dom節(jié)點(diǎn)的過程中,我們也會(huì)相對(duì)應(yīng)的實(shí)現(xiàn)對(duì)該dom元素的數(shù)據(jù)綁定,以求在最后直接添加到為根節(jié)點(diǎn)的子元素即可,這個(gè)過程我們就在nodeToFragment函數(shù)中插入了compile函數(shù)來初始化綁定,并且添加遞歸函數(shù)實(shí)現(xiàn)所有子元素的初始綁定;
4、在compile函數(shù)中我們添加的數(shù)據(jù)又從何而來呢?對(duì),正是因?yàn)檫@點(diǎn),所以我們建立MVVM的構(gòu)造函數(shù)Vue來實(shí)現(xiàn)數(shù)據(jù)支持,并實(shí)現(xiàn)在實(shí)例化時(shí)就執(zhí)行nodeToFragment同時(shí)重構(gòu)dom和實(shí)現(xiàn)初始化綁定compile;
5、好了,單向綁定就是這么簡(jiǎn)單,4個(gè)函數(shù)即可Vue => nodeToFragment => compile => isChild。
完成圖如下
好了,再回過來看看整體的流程圖,我們已經(jīng)實(shí)現(xiàn)了這一塊了
接下來,休息下,大家準(zhǔn)備開始流程圖后面的雙向綁定,ok,還是按照單向綁定的順序,先跟大家講明實(shí)現(xiàn)邏輯;
1、創(chuàng)建數(shù)據(jù)監(jiān)聽者observer去監(jiān)聽view層數(shù)據(jù)的變化;(利用Object.defineProperty劫持所有要用到的數(shù)據(jù))
2、當(dāng)view層數(shù)據(jù)變化后,通過通知者Dep通知訂閱者去實(shí)現(xiàn)數(shù)據(jù)的更新;(通知后,遍歷所有用到數(shù)據(jù)的訂閱者更新數(shù)據(jù))
3、訂閱者watcher接收到view層數(shù)據(jù)變更后,重新對(duì)變化的數(shù)據(jù)進(jìn)行賦值,改變model層,從而改變所有view層用到過該數(shù)據(jù)的地方。(更新數(shù)據(jù),并改變view層所有用到該數(shù)據(jù)的節(jié)點(diǎn)值)
上面是實(shí)現(xiàn)邏輯,下面將通過具體代碼告訴大家每一步的做法,由于雙向綁定中訂閱者會(huì)涉及初始化綁定的過程,所以代碼量較多,我會(huì)在大更改處用——為大家框出來
//判斷每個(gè)dom節(jié)點(diǎn)是否擁有子節(jié)點(diǎn),若有則返回該節(jié)點(diǎn) function isChild(node){ if(node.childNodes.length ===0){ return false; } else{ return node; } } //利用文檔碎片劫持dom結(jié)構(gòu)及數(shù)據(jù),進(jìn)而進(jìn)行dom的重構(gòu) function nodeToFragment(node,vm){ var frag = document.createDocumentFragment(); var child; while(child = node.firstChild){ //一級(jí)dom節(jié)點(diǎn)數(shù)據(jù)綁定 compile(child,vm); //判斷每個(gè)一級(jí)dom節(jié)點(diǎn)是否有二級(jí)節(jié)點(diǎn),若有則遞歸處理文檔碎片 if(isChild(child)){ nodeToFragment(isChild(child),vm); } frag.appendChild(child); } node.appendChild(frag); } //初始化綁定數(shù)據(jù) function compile(node,vm){ //node節(jié)點(diǎn)為元素節(jié)點(diǎn)時(shí) 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標(biāo)簽 //------------------------ if(node.nodeName === 'INPUT'){ node.addEventListener('keyup',function(e){ vm[name] = e.target.value; }) } //由于數(shù)據(jù)已經(jīng)由data劫持至vm下,所以直接賦值vm[name]即可觸發(fā)getter訪問器 node.value = vm[name]; //------------------------- node.removeAttribute(attr[i].nodeName); } } } //node節(jié)點(diǎn)為text文本節(jié)點(diǎn)時(shí) if(node.nodeType === 3){ var reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue.trim())){ var name = RegExp.$1; //node.nodeValue = vm[name]; //---------------------- //為每個(gè)節(jié)點(diǎn)建立訂閱者,通過訂閱者watcher初始化及更新視圖數(shù)據(jù) new watcher(vm,node,name); //----------------------- } } } //---------------------------------------------------------------- //訂閱者(為每個(gè)節(jié)點(diǎn)的數(shù)據(jù)建立watcher隊(duì)列,每次接受更改數(shù)據(jù)需求后,利用劫持?jǐn)?shù)據(jù)執(zhí)行對(duì)應(yīng)節(jié)點(diǎn)的數(shù)據(jù)更新) function watcher(vm,node,name){ //將每個(gè)掛載了數(shù)據(jù)的dom節(jié)點(diǎn)添加到通知者列表,要保證每次創(chuàng)建watcher時(shí)只有一個(gè)添加目標(biāo),否則后續(xù)會(huì)因?yàn)閣atcher是全局而被覆蓋,所以每次要清空目標(biāo) Dep.target = this; this.vm = vm; this.node = node; this.name = name; //執(zhí)行update的時(shí)候會(huì)調(diào)用監(jiān)聽者劫持的getter事件,從而添加到watcher隊(duì)列,因?yàn)閡pdate中有訪問this.vm[this.name] this.update(); //為保證只有一個(gè)全局watcher,添加到隊(duì)列后,清空全局watcher Dep.target = null; } watcher.prototype = { update(){ this.get(); //input標(biāo)簽特殊處理化 if(this.node.nodeName === 'INPUT'){ this.node.value = this.value; } else{ this.node.nodeValue = this.value; } }, get(){ //這里調(diào)用了數(shù)據(jù)劫持的getter this.value = this.vm[this.name]; } }; //通知者(將監(jiān)聽者的更改信息需求發(fā)送給訂閱者,告訴訂閱者哪些數(shù)據(jù)需要更改) function Dep(){ this.subs = []; } Dep.prototype = { addSub(watcher){ //添加用到數(shù)據(jù)的節(jié)點(diǎn)進(jìn)入watcher隊(duì)列 this.subs.push(watcher); }, notify(){ //遍歷watcher隊(duì)列,令相應(yīng)數(shù)據(jù)節(jié)點(diǎn)重新更新view層數(shù)據(jù),model => view this.subs.forEach(function(watcher){ watcher.update(); }) } }; //監(jiān)聽者(利用setter監(jiān)聽view => model的數(shù)據(jù)變化,發(fā)出通知更改model數(shù)據(jù)后再?gòu)膍odel => view更新視圖所有用到該數(shù)據(jù)的地方) 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(){ //初始化數(shù)據(jù)更新時(shí)將每個(gè)數(shù)據(jù)的watcher添加至隊(duì)列棧中 if(Dep.target) dep.addSub(Dep.target); return val; }, set(newVal){ if(val === newVal) return ; //初始化后,文檔碎片中的虛擬dom已與model層數(shù)據(jù)綁定起來了 val = newVal; //同步更新model中data屬性下的數(shù)據(jù) vm.data[key] = val; //數(shù)據(jù)有改動(dòng)時(shí)向通知者發(fā)送通知 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' } })
好的,到這里雙向綁定的講解也就結(jié)束了,代碼量確實(shí)有點(diǎn)多,但是我們看到其實(shí)邏輯在你熟悉后并不復(fù)雜,特別是參照了上文的流程圖后,其實(shí)就是:
1、通過observer劫持所有model層數(shù)據(jù)到vue下,并在劫持時(shí)靈活運(yùn)用getter與setter訪問器屬性來在虛擬dom初始化數(shù)據(jù)綁定時(shí),利用此時(shí)的get方法綁定初始化數(shù)據(jù)進(jìn)入通知者隊(duì)列,后續(xù)初始化完成后,在view層數(shù)據(jù)發(fā)生變化時(shí),利用set方法及時(shí)利用通知者發(fā)出通知;
2、在dep通知者接收到有一處dom節(jié)點(diǎn)數(shù)據(jù)更改的通知時(shí),遍歷watcher隊(duì)列及告訴watcher訂閱者,view層數(shù)據(jù)有所變動(dòng)model層已經(jīng)相應(yīng)改變,你要重新執(zhí)行update將model層的數(shù)據(jù)更新到view層所有用到該數(shù)據(jù)的地方(比如我們利用input實(shí)現(xiàn)的雙向綁定就不止一個(gè)dom節(jié)點(diǎn)內(nèi)使用了,而是多個(gè),所以必須整體遍歷修改)。
3、這是一個(gè)model => view => model =>view的過程,真正的邏輯順序?yàn)閙odel => view,view => model,model => view,反復(fù)的過程~
貼上結(jié)果圖
初始未改動(dòng)view層數(shù)據(jù)圖
修改view層數(shù)據(jù)后圖
最后大家再看一次流程圖,看看整個(gè)邏輯是不是跟流程圖一模一樣,通過流程圖就可以回憶起這個(gè)邏輯過程,寫多2遍就可以記??!
以上只是通過簡(jiǎn)單實(shí)現(xiàn)來告訴大家vue的數(shù)據(jù)劫持+訂閱發(fā)布模式這個(gè)雙向綁定的原理,其中有很多細(xì)節(jié)上的不足可能未作處理,還請(qǐng)見諒~
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
使用ElementUI修改el-tabs標(biāo)簽頁(yè)組件樣式
這篇文章主要介紹了使用ElementUI修改el-tabs標(biāo)簽頁(yè)組件樣式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08原生JS實(shí)現(xiàn)Vue transition fade過渡動(dòng)畫效果示例
這篇文章主要為大家介紹了原生JS實(shí)現(xiàn)Vue transition fade過渡動(dòng)畫效果示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06axios解決高并發(fā)的方法:axios.all()與axios.spread()的操作
這篇文章主要介紹了axios解決高并發(fā)的方法:axios.all()與axios.spread()的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11Vue Element前端應(yīng)用開發(fā)之echarts圖表
在我們做應(yīng)用系統(tǒng)的時(shí)候,往往都會(huì)涉及圖表的展示,綜合的圖表展示能夠給客戶帶來視覺的享受和數(shù)據(jù)直觀體驗(yàn),同時(shí)也是增強(qiáng)客戶認(rèn)同感的舉措之一2021-05-05關(guān)于vue中hash和history的區(qū)別與使用圖文詳解
vue-router中有hash模式和history模式,下面這篇文章主要給大家介紹了關(guān)于vue中hash和history的區(qū)別與使用的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03詳解用vue-cli來搭建vue項(xiàng)目和webpack
本篇文章主要介紹了詳解用vue-cli來搭建vue項(xiàng)目和webpack,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04使用elementuiadmin去掉默認(rèn)mock權(quán)限控制的設(shè)置
這篇文章主要介紹了使用elementuiadmin去掉默認(rèn)mock權(quán)限控制的設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04