摘要:發布訂閱現在每個人應該都用微信吧,一個人可以關注多個公眾號,多個人可以同時關注相同的公眾號。公眾號每周都會更新內容,并推送給我們,把寫好的文章在微信管理平臺更新就好了,點擊推送,就相當于發布。
什么是MVVM
MVVM——Model-View-ViewModle的縮寫,MVC設計模式的改進版。Model是我們應用中的數據模型,View是我們的UI層,通過ViewModle,可以把我們Modle中的數據映射到View視圖上,同時,在View層修改了一些數據,也會反應更新我們的Modle。
上面的話,未免太官方了。簡單理解就是雙向數據綁定,即當數據發生變化的時候,視圖也就發生變化,當視圖發生變化的時候,數據也會跟著同步變化。
MVVM這種思想的前端框架其實老早就有了,我記得是在13年,自己在公司的主要工作是做后臺管理系統的UI設計和開發,當時就思考,如何讓那些專注后臺的開發,既簡單又方便的使用前端開發的一些組件。當時有三種方案:
使用Easy-ui,但easy-ui好像官方要求收費,當然也可以破解使用
自己開發UI框架,其實當時想做的東西就是后來BootStrap
使用谷歌的Angular,進行二次開發
后來的評估是:
使用easy-ui,工作量太多
使用Angular和easy-ui不僅工作量很大,后臺也要做相應的修改
自己寫UI框架,比較合適,當時的做法是寫一些jQuery相關的插件,先給后臺一個js插件包,后續的UI修改,慢慢進行。
當時自己還是比較推崇Angular的,我記得后來還買了一本《基于MVC的Javascript Web富應用開發》專門去了解這種模式在工作中可能用的情況,以及實現它的一些基本思路。
當時熱點比較高的MVVM框架有:
Angular:谷歌出品,名氣很大,入門高,使用麻煩,它提供了很多新的概念。
Backbone.js,入門要求級別很高,我記得當時淘寶有些項目應用了這個,《基于MVC富應用開發》書里面也是以這個框架為主介紹MVC的。
Ember:大而全的框架,開始寫代碼之前就已經有很多的工作要做了。
當年的環境和條件都沒有現在好,無論從技術完善的情況,還是工作的實際情況上面看,都是如此——那時候前后端分離都是理想。
當然現在環境好了,各種框架的出現也極大方便了我們,提高了我們開發的工作效率。時代總是在進步,大浪淘沙,MVVM的框架現在比較熱門和流行的,我相信大家現在都知道,就是下面三種了:
Angular
Vue
React
現在Angular除了一些忠實的擁躉,基本上也就沒落了。Angular無論從入門還是實際應用方面,都要比其他兩個框架發費的時間成本更大。
Angular現在有種英雄末路的感覺,但不能不承認,之前它確實散發了光芒。
Angular的1.x版本,是通過臟值檢測來實現雙向綁定的。
而最新的Angular版本和Vue,以及React都是通過數據劫持+發布訂閱模式來實現的。
臟值檢測
簡單理解就是,把老數據和新數據進行比較,臟就表示之前存在過,有過痕跡,通過比較新舊數據,來判斷是否要更新。感興趣的可以看看這篇文章 構建自己的AngularJS,第一部分:作用域和digest。
數據劫持 發布訂閱
數據劫持:在訪問或者修改對象的某個屬性時,通過代碼攔截這個行為,進行額外的操作或者修改返回結果。在ES5當中新增了Object.defineProperty()可以幫我們實現這個功能。
發布訂閱:現在每個人應該都用微信吧,一個人可以關注多個公眾號,多個人可以同時關注相同的公眾號。關注的動作就相當于訂閱。公眾號每周都會更新內容,并推送給我們,把寫好的文章在微信管理平臺更新就好了,點擊推送,就相當于發布。更詳細的可以深入閱讀 javascript設計模式——發布訂閱模式
怎么實現一個MVVM我們靜下心好好思考下,如果才能實現雙向數據綁定的功能。可能需要:
一個初始化實例的類
一個存放數據的對象Object
一個可以把我們的數據映射到HTML頁面上的“模板解析”工具
一個更新數據的方法
一個通過監聽數據的變化,更新視圖的方法
一個掛載模板解析的HTML標簽
通過上面這樣的思考,我們可以簡單的寫一下大概的方法。
class MVVM { constructor(data){ this.$option = option; const data = this._data = this.$option.data; //數據劫持 observe(data) //數據代理 proxyData(data) //編譯模板 const dom = this._el = this.$option.el; complie(dom,this); //發布訂閱 //連接視圖和數據 //實現雙向數據綁定 } } // Observe類 function Observe(){} // Observe實例化函數 function observe(data){ return new Observe(data); } // Compile類 function Compile(){} // Compile實例化函數 function compile(el){ return new Compile(el) }數據劫持
我們有下面這樣一個對象
let obj = { name:"mc", age:"29", friends:{ name:"hanghang", name:"jiejie" } }
我們要對這個對象執行某些操作(讀取,修改),通常像下面就可以
// 取值 const name = obj.name; console.log(obj.age) const friends = obj.friends; // 修改 obj.name = "mmcai"; obj.age = 30;
在VUE中,我們知道,如果data對象中的某個屬性,在template當中綁定的話,當我們修改了這個屬性值,我們的視圖也就更新了。這就是雙向數據綁定,數據變化,視圖更新,同時反過來也一樣。
要實現這個功能,我們就需要知道data當中的數據是如何變動了,ES5當中提供了Object.defineProperty()函數,我們可以通過這個函數對我們data對象當中的數據進行監聽。當數據變動,就會觸發這個函數里面的set方法,通過判斷數據是否變化,就可以執行一些方法,更新我們的視圖了。所以我們現在需要實現一個數據監聽器Observe,來對我們data中的所有屬性進行監聽。
// Observe類的實例化函數 function observe(data){ // 判斷數據是否是一個對象 if(typeof data !== "object"){ return; } // 返回一個Observe的實例化對象 return new Observe(data) } // Observer類的實現 class Observe{ constructor(data){ this.data = data; this.init(data) } init(data){ for(let k in data){ let val = data[k]; //如果data是一個對象,我們遞歸調用自身 if(typeof val === "object"){ observe(val); } Object.defineProperty(data,k,{ enumerable:true, get(){ return val; }, set(newVal){ //如果值相同,直接返回 if(newVal === val){ return; }; //賦值 val = newVal; //如果新設置的值是一個對象,遞歸調用observe方法,給新數據也添加上監聽 if(typeof newVal === "object"){ observe(newVal); } } }) } } }
了解了數據劫持,我們就可以明白,為什么我們實例化vue的時候,必須事先在data當中定義好我們的需要的屬性了,因為我們新增的屬性,沒有經過observe進行監聽,沒有通過observe監聽,后面complie(模板解析)也就不會執行。
所以,雖然你可以在data上面設置新的屬性,并讀取,但視圖卻不能更新。
數據代理我們常見的代理有nginx,就是我們不直接去訪問(操作)我們實際要訪問的數據,而是通過訪問一個代理,然后代理幫我們去拿我們真正需要的數據。
一般的特點是:
安全,不把真實內容暴露
方便,可以把一些復雜的操作,通過代理進行簡化
...
下面是VUE簡單的一個使用實例:
cosnt vm = new Vue({ el:"#app", data:{ name:"mmcai" } });
我們的實例化對象vm,想要讀取data里面的數據的時候,不做任何處理的正常情況下,使用下面方式讀取:
const name = vm.data.name;
這樣操作起來,顯然麻煩了一些,我們就可以通過數據代理,直接把data綁定到我們的實例上,所以在vue當中,我們一般獲取數據像下面一樣:
cosnt vm = new Vue({ el:"#app", data:{ name:"mmcai" }, created(){ // 直接通過實例就可以訪問到data當中的數據 const name = this.name; // 通過this.data.name 也可以訪問,但是顯然,麻煩了一些 } });
同樣,我們通過Object.defineProperty函數,把data對象中的數據,綁定到我們的實例上就可以了,代碼如下:
class MVVM { constructor(option){ //此處代碼省略 this.$option = option; const data = this._data = this.$option.data; //調用代理 this._proxyData(data); } _proxyData(data){ const that = this; for(let k in data){ let val = data[k]; Object.defineProperty(that,k,{ enumerable:true, get(){ return that._data[k]; }, set(newVal){ that._data[k] = newVal; } }) } } }編譯模板
利用正則表達式識別模板標識符,并利用數據替換其中的標識符。
VUE里面的標識符是 {{}} 雙大括號,數據就是我們定義在data上面的內容。
實現原理
確定我們的模板范圍
遍歷DOM節點,循環找到我們的標識符
將標識符的內容用數據進行填充填充
遍歷解析需要替換的根元素el下的HTML標簽,一定會使用遍歷對DOM節點進行操作,對DOM操作就會引發頁面的重排和重繪,為了提高性能和效率,可以把el根節點下的所有節點替換為文檔碎片fragment進行解析編譯操作,解析完成,再將fragment添加到根節點el中
如果想對文檔碎片進行,更多的了解,可以查看文章底部的參考資料
class Complie{ constructor(el,vm){ this.$vm = vm; this.$el = document.querySelector(el); //第一步,把DOM轉換成文檔碎片 this.$fragment = this.nodeToFragment(this.$el); //第二步,匹配標識符,填充數據 this.compileElement(this.$fragment); //把文檔碎片,添加到el根節點上面 this.$el.appendChild(this.$fragment); } // 把DOM節點轉換成文檔碎片 nodeToFragment(el){ let nodeFragment = document.createDocumentFragment(); // 循環遍歷el下面的節點,填充到文檔碎片nodeFragment中 while(child = el.firstChild){ nodeFragment.appendChild(child); } // 把文檔碎片返回 return nodeFragment; } // 遍歷目標,查找標識符,并替換 compileElement(node){ let reg = /{{(.*)}}/; Array.from(node.childNodes).forEach((node)=>{ let text = node.textContent; if(node.nodeType === 3 && reg.test(text)){ let arr = RegExp.$1.split("."); // vm 是實例的整個data對象 let val = vm; arr.forEach((k)=>{ val = val[k] }) node.textContent = text.replace(/{{(.*)}}/,val); } // 如果節點包含字節的,遞歸調用自身 if(node.childNodes){ this.compileElement(node) } }) } } const complie = (el,vm)=>{ return new Compile(el,vm) }發布訂閱
在軟件架構中,發布訂閱是一種消息范式,消息的發送者(成為發布者)不會將消息直接發送給特定的接收者(成為訂閱者)。二十將發布的消息分為不同的類別,無需了解哪些訂閱者是否存在。同樣的,訂閱者可以表達對一個或多個類別的興趣,直接受感興趣的消息,無需了解哪些發布者是否存在——維基。
上述的表達中,既然說發布者不關心訂閱者,訂閱者也不關心發布者,那么他們是如何通信呢?
其實就是通過第三方,通常在函數中我們,稱他們為觀察者watcher
在VUE的里面,我們要確認幾個概念,誰是發布者,誰是訂閱者,為什么需要發布訂閱?
上面我們說了數據劫持Observe,也說了Compile,其實,Observe和Compile 他們即使發布者,也是訂閱者,幫助他們之間的通訊,就是watcher的工作。
通過下面的代碼,我們簡單了解下,發布訂閱模式的實現情況。
// 創建一個類 // 發布訂閱,本質上是維護一個函數的數組列表,訂閱就是放入函數,發布就是讓函數執行 class Dep{ consturctor(){ this.subs=[]; } // 添加訂閱者 addSub(sub){ this.subs.push(sub); } // 通知訂閱者 notify(){ // 訂閱者,都有 this.subs.forEach((sub=>sub.update()); } } // 監聽函數,watcher // 通過Watcher類創建的實例,都有update方法 class Watcher{ // watcher的實例,都需要傳入一個函數 constructor(fn){ this.fn = fn; } // watcher的實例,都擁有update方法 update(){ this.fn(); } } // 把函數作為參數傳入,實例化一個watcher const watcher = new Watcher(()=>{ consoole.log("1") }); // 實例化Dep 類 const dep = new Dep(); // 將watcher放到dep維護的數組中,watcher實例本身具有update方法 // 可以理解成函數的訂閱 dep.addSub(watcher); // 執行,可以理解成,函數的發布, // 不關心,addSub方法訂閱了誰,只要訂閱了,就通過遍歷循環subs數組,執行數組每一項的update dep.notify();
通過以上代碼的了解,我們繼續實現我們MVVM中的代碼,實現數據和視圖的關聯。
這種關聯的結果就是,當我們修改data中的數據的時候,我們的視圖更新。或者我們視圖中修改了相關內容,我們的data也進行相關的更新,所以這里主要的邏輯代碼,就是我們watcher當中的update方法。
我們根據上面的內容,對我們的Observe和Compile以及Watcher進行修改,代碼如下:
class MVVM{ constructor(option){ this.$option = option; const data = this._data = this.$option.data; this.$el = this.$option.el; // 數據劫持 this._observe(data); // 數據代理 this._proxyData(data); //模板解析 this._compile(this.$el,this) } // 數據代理 _proxyData(data){ for(let k in data){ let val = data[k]; Object.defineProperty(this,k,{ enumerable:true, get(){ return this._data[k]; }, set(newVal){ this._data[k] = newVal; } }) } } } // 數據劫持 class Observe{ constructor(data){ this.init(data); } init(data){ let dep = new Dep(); for(let k in data){ let val = data[k]; // val 可能是一個對象,遞歸調用 if(typeof val === "object"){ observe(val); } Object.defineProperty(data,k,{ enumerable:true, get(){ // 訂閱, // Dep.target 是Watcher的實例 Dep.target && dep.addSub(Dep.target); return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; observe(newVal); dep.notify(); } }) } } } // 數據劫持實例 function observe(data){ if(typeof data !== "object"){ return }; return new Observe(data); } // 模板編譯 class Compile{ constructor(el,vm){ vm.$el = document.querySelector(el); //1.把DOM節點,轉換成文檔碎片 const Fragment = this.nodeToFragment(vm.$el) //2.通過正則匹配,填充數據 this.replace(Fragment,vm); //3.把填充過數據的文檔碎片,插入模板根節點 vm.$el.appendChild(Fragment); } // DOM節點轉換 nodeToFragment(el){ // 創建文檔碎片, const fragment = document.createDocumentFragment(); //遍歷DOM節點,把DOM節點,添加到文檔碎片上 while(child ===el.firstChild){ fragment.appendChild(child); } // 返回文檔碎片 return fragment; } //匹配標識,填充數據 replace(fragment,vm){ // 使用Array.from方法,把DOM節點,轉化成數據,進行循環遍歷 Array.from(fragment.childNodes).forEach((node)=>{ // 遍歷節點,拿到每個內容節點 let text = node.textContent; // 定義標識符的正則 let reg = /{{(.*)}}/; //如果節點是文本,且節點的內容當中匹配到了模板標識符 // 數據渲染視圖 if(node.nodeType===3 && reg.test(text)){ // 用數據替換標識符 let arr = RegExp.$1.split("."); let val = vm; arr.forEach((item)=>{ val = val[item]; }) // 添加一個watcher,當我們的數據發生變化的時候,更新我們的view new Watcher(vm,RegExp.$1,(newVal)=>{ node.textContent = text.replace(reg,newVal); }) //把數據填充到節點上 node.textContent = text.replace(reg,val); } // 視圖更新數據 if(node.nodeType === 1){ let nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach((attr)=>{ let name = attr.name; // 獲取標識符的內容,也就是v-mode="a"的內容 let exp = attr.value; if(name.indexOf("v-model")===0){ node.value = vm[exp]; }; new Watcher(vm,exp,(newVal)=>{ node.value = newVal; }); node.addEventListener("input",function(e){ let newVal = e.target.value; vm[exp] = newVal; }); }); } // 如果節點包含子節點,遞歸調用自身 if(node.childNodes){ this.replace(node,vm); } }) } } // 模板編譯實例 function compile(el,vm){ return new Compile(el,vm) } // 發布訂閱 class Dep{ constructor(){ this.subs = []; } // 訂閱函數 addSub(fn){ this.subs.push(fn); } // 發布執行函數 notify(){ this.subs.forEach((fn)=>{ fn(); }) } } // Dep實例 function dep(){ return new Dep(); } // 觀察者 class Watcher{ // vm,我們的實例 // exp,我們的標識符 // fn,回調 constructor(vm,exp,fn){ this.fn = fn; this.vm = vm; this.exp = exp; Dep.target = this; let val = vm; let arr = exp.split("."); arr.forEach((k)=>{ val = val[k] }); // 完成之后,我們把target 刪除; Dep.target = null; } update(){ let val = this.vm; let arr = this.exp.split("."); arr.forEach((k)=>{ val = val[k]; }) this.fn(); } } function watcher(){ return new Watcher() }
Wathcer干了那些好事:
在自身實例化的時候,往訂閱器(dep)里面添加自己
自身有一個update方法
待data屬性發生修改的時候,dep.notify()通知的時候,可以調用自身的update()方法,在update()方法出發綁定的回調
Watcher連接了兩個部分,包括Observe和Compile;
在Observe方法執行的時候,我們給data的每個屬性都添加了一個dep,這個dep被閉包在get/set函數內。
當我們new Watcher,在之后訪問data當中屬性的時候,就會觸發通過Object.defineProperty()函數當中的get方法。
get方法的調用,就會在屬性的訂閱器實例dep中,添加當前Watcher的實例。
當我們嘗試修改data屬性的時候,就會出發dep.notify()方法,該方法會調用每個Watcher實例的update方法,從而更新我們的視圖。
結束語回顧下整個MVVM實現的整個過程
使用Object.defineProperty()函數,給每個data屬性添加get/set,并為每個屬性創建一個dep實例,監聽數據變化
同樣使用Object.defineProperty()函數,把data對象的屬性,綁定到我們MVVM實例vm對象上,簡化使用
通過document.createDocumentFragment,把我們el節點下的dom轉換成文檔碎片
遍歷文檔碎片,找到模板標識符,進行數據的替換,添加Watcher觀察者,當數據發生變化的時候,再次更新我們的文檔碎片
把文檔碎片插入到我們的el節點中。
我們修改data,執行dep.notify()方法,然后調用Watcher實例上的update方法,更新視圖。
我這里有一個簡短的視頻,是某培訓機構講解MVVM的內容,大家有興趣,可以自取。
視頻鏈接
提取碼:1i0r
如果失效,可以私聊我。
參考廖雪峰談MVVM
...,讓MVVM原理還給你
觀察者模式與發布訂閱模式
基于vue實現一個簡單的MVVM框架
文檔碎片
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104707.html
摘要:最近,由于公司項目需要,使用百度框架開發了一個兼容的小項目。是框架,和有一些類似。和相比,優勢是能兼容,但沒有那么簡單易用,學習最好有一些框架的基礎。當初我自己好奇,嘗試用做了一個小型項目,這里記錄一下。 最近,由于公司項目需要,使用百度mvvm框架san開發了一個兼容ie6的小項目。san是mvvm框架,和vue有一些類似。和vue相比,優勢是能兼容ie6,但沒有vue那么簡單易用,...
摘要:的數據劫持版本內部使用了來實現數據與視圖的雙向綁定,體現在對數據的讀寫處理過程中。這樣就形成了數據的雙向綁定。 MVVM由以下三個內容組成 View:視圖模板 Model:數據模型 ViewModel:作為橋梁負責溝通View和Model,自動渲染模板 在JQuery時期,如果需要刷新UI時,需要先取到對應的DOM再更新UI,這樣數據和業務的邏輯就和頁面有強耦合。 在MVVM中,U...
摘要:總結這邊文章主要是介紹了下的實例與生命周期,在實例化的過程中我們可以添加許多可選對象,比如生命周期鉤子函數等,讓實例產生我們想要的行為。 理解與認識 Vue 的實例是我們學習 Vue 非常重要的一步,也是非常必須的,因為實例是它的一個起點,也是它的一個入口,只有我們創建一個 Vue 實例之后,我們才行利用它進行一些列的操作。 首先 Vue 沒有完全遵守 MVVM 的架構模式,但是它的設...
摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當天就進行了修復,現在最新的代碼里已經不是這個樣子了而且狀態機標識由字符串換成了數字常量,解析更準確的同時執行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學習了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結構梳...
閱讀 3061·2021-11-23 09:51
閱讀 1040·2021-09-02 15:21
閱讀 3005·2019-08-30 13:56
閱讀 1829·2019-08-29 14:12
閱讀 708·2019-08-29 13:53
閱讀 1664·2019-08-29 11:32
閱讀 1325·2019-08-29 11:25
閱讀 1493·2019-08-28 17:51