相信大家對MVVM雙向綁定應該都不陌生了,一言不合上代碼,下面先看一個本文終實現的效果吧,和Vue一樣的語法,如果還不了解雙向綁定,猛戳 Google。
效果:
目前幾種主流的MVC(VM)框架都實現了單向數據綁定,而我所理解的雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model和view,并沒有多高深。所以無需太過介懷是實現的單向或雙向綁定。
實現數據綁定的做法有大致如下幾種:
發布者-訂閱者模式: 一般通過sub, pub的方式實現數據和視圖的綁定監聽,更新數據方式通常做法是vm.set('property', value),這里有篇文章講的比較詳細,有興趣可點擊查看。
但我們更希望通過vm.property = value這種方式更新數據,同時自動更新視圖,于是有了下面兩種方式:
臟值檢查:Angular.js是通過臟值檢測的方式比對數據是否有變更,來決定是否更新視圖,簡單的方式就是通過setInterval()定時輪詢檢測數據變動,當然Google不會這么low,Angular只有在指定的事件觸發時進入臟值檢測,大致如下:
ng-click)
$http)
$location)
$timeout , $interval)
$digest()或$apply()
數據劫持:Vue.js則是采用數據劫持結合發布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。
已經了解到vue是通過數據劫持的方式來做數據綁定的,其中核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,達到監聽數據變動的目的,無疑這個方法是本文中重要、基礎的內容之一,如果不熟悉defineProperty,可以點擊此處查看。
整理了一下,要實現MVVM的雙向綁定,就必須要實現以下幾點:
上述流程如圖所示:
1、實現Observer
ok, 思路已經整理完畢,也已經比較明確相關邏輯和模塊功能了,let’s do it!
我們知道可以利用Obeject.defineProperty()來監聽屬性變動
那么將需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上setter和getter,這樣的話,給這個對象的某個值賦值,就會觸發setter,那么就能監聽到數據變化。相關代碼可以是這樣:
這樣我們已經可以監聽每個數據的變化了,那么監聽到變化之后就是怎么通知訂閱者了,所以接下來我們需要實現一個消息訂閱器,很簡單,維護一個數組,用來收集訂閱者,數據變動觸發notify,再調用訂閱者的update方法,代碼改善之后是這樣:
那么問題來了,誰是訂閱者?怎么往訂閱器添加訂閱者?
沒錯,上面的思路整理中我們已經明確訂閱者應該是Watcher,而且var dep = new Dep();是在defineReactive方法內部定義的,所以想通過dep添加訂閱者,就必須要在閉包內操作,所以我們可以在getter里面動手腳:
這里已經實現了一個Observer了,已經具備了監聽數據和數據變化通知訂閱者的功能。那么接下來就是實現Compile了。
2、實現Compile
Compile主要做的事情是解析模板指令,將模板中的變量替換成數據,然后初始化渲染頁面視圖,并將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,如圖所示:
因為遍歷解析的過程有多次操作dom節點,為提高性能和效率,會先將跟節點el轉換成文檔碎片fragment進行解析編譯操作,解析完成,再將fragment添加回原來的真實dom節點中:
compileElement方法將遍歷所有節點及其子節點,進行掃描解析編譯,調用對應的指令渲染函數進行數據渲染,并調用對應的指令更新函數進行綁定,詳看代碼及注釋說明:
這里通過遞歸遍歷保證了每個節點及子節點都會解析編譯到,包括了{{}}表達式聲明的文本節點。指令的聲明規定是通過特定前綴的節點屬性來標記,如<span v-text="content" other-attr中v-text便是指令,而other-attr不是指令,只是普通的屬性。
監聽數據、綁定更新函數的處理是在compileUtil.bind()這個方法中,通過new Watcher()添加回調來接收數據變化的通知。
至此,一個簡單的Compile就完成了。接下來要看看Watcher這個訂閱者的具體實現了。
3、實現Watcher
Watcher訂閱者作為Observer和Compile之間通信的橋梁,主要做的事情是:
如果有點亂,可以回顧下前面的思路整理:
實例化Watcher的時候,調用get()方法,通過Dep.target = watcherInstance標記訂閱者是當前Watcher實例,強行觸發屬性定義的getter方法,getter方法執行的時候,就會在屬性的訂閱器dep添加當前Watcher實例,從而在屬性值有變化的時候,watcherInstance就能收到更新通知。
Ok,Watcher也已經實現了。
基本上Vue中數據綁定相關比較核心的幾個模塊也是這幾個,點擊此處,在src目錄可找到Vue源碼。后來講講MVVM入口文件的相關邏輯和實現吧,相對就比較簡單了。
4、實現MVVM
MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。
一個簡單的MVVM構造器是這樣子:
但是這里有個問題,從代碼中可看出監聽的數據對象是options.data,每次需要更新視圖,則必須通過var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq';這樣的方式來改變數據。
顯然不符合我們一開始的期望,我們所期望的調用方式應該是這樣的:var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';
所以這里需要給MVVM實例添加一個屬性代理的方法,使訪問VM的屬性代理為訪問vm._data的屬性,改造后的代碼如下:
這里主要還是利用了Object.defineProperty()這個方法來劫持了VM實例對象的屬性的讀寫權,使讀寫VM實例的屬性轉成讀寫了vm._data的屬性值,達到魚目混珠的效果。
至此,全部模塊和功能已經完成了,如本文開頭所承諾的兩點。一個簡單的MVVM模塊已經實現,其思想和原理大部分來自經過簡化改造的Vue源碼,在這里可以看到本文的所有相關代碼。
由于本文內容偏實踐,所以代碼量較多,且不宜列出大篇幅代碼,所以建議想深入了解的童鞋可以再次結合本文源代碼來進行閱讀,這樣會更加容易理解和掌握。
本文主要圍繞“幾種實現雙向綁定的做法”、“實現Observer”、“實現Compile”、“實現Watcher”、“實現MVVM”這幾個模塊來闡述雙向綁定的原理和實現。并根據思路流程漸進梳理講解了一些細節思路和比較關鍵的內容點,以及通過展示部分關鍵代碼講述怎樣一步步實現一個雙向綁定MVVM。文中可能會有一些不夠嚴謹的思考和錯誤,歡迎大家指正,有興趣歡迎一起探討和改進。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。