若是你對於MVVM的造成不是特別清晰,則能夠先閱讀如下部分。javascript
本文能夠幫助你瞭解什麼?html
咱們先來花點時間想一想,若是你是一個前端框架(Vue、React或者Angular)的開發者,你是有多麼頻繁的聽到「MVVM」這個詞,但你真正明白它的含義嗎?前端
起初計算機科學家(如今的咱們是小菜雞)在設計GUI(圖形用戶界面)應用程序的時候,代碼是雜亂無章的,一般難以管理和維護。GUI的設計結構通常包括視圖(View)、模型(Model)、邏輯(Application Logic、Business Logic以及Sync Logic),例如:vue
能夠發如今GUI中視圖和模型是自然能夠進行分層的,雜亂無章的部分主要是邏輯。因而咱們的程序員們不斷的絞盡腦汁在想辦法優化GUI設計的邏輯,而後就出現了MVC、MVP以及MVVM等設計模式。java
在B/S架構的應用開發中,MV*設計模式概述並封裝了應用程序及其環境中須要關注的地方,儘管JavaScript已經變成一門同構語言,可是在瀏覽器和服務器之間這些關注點可能不同:node
早在上個世紀70年代,美國的施樂公司(Xerox)的工程師研發了Smalltalk編程語言,而且開始用它編寫GUI。而在Smalltalk-80版本的時候,一位叫Trygve Reenskaug的工程師設計了MVC的架構模式,極大地下降了GUI的管理難度。git
如圖所示,MVC把GUI分紅View(視圖)、Model(模型)、Controller(控制 器)(可熱插拔,主要進行Model和View之間的協做,包括路由、輸入預處理等業務邏輯)三個模塊:程序員
Model的更新經過觀察者模式,能夠實現多視圖共享同一個Model。github
傳統的MVC設計對於Web前端開發而言是一種十分有利的模式,由於View是持續性的,而且View能夠對應不一樣的Model。Backbone.js就是一種稍微變種的MVC模式實現(和經典MVC較大的區別在於View能夠直接操做Model,所以這個模式不能同構)。這裏總結一下MVC設計模式可能帶來的好處以及不夠完美的地方:算法
優勢:
缺點:
####服務端MVC
經典MVC只用於解決GUI問題,可是隨着B/S架構的不斷髮展,Web服務端也衍生出了MVC設計模式。
JSP Model1是早期的Java動態Web應用技術,它的結構以下所示:
在Model1中,JSP同時包含了Controller和View,而JavaBean包含了Controller和Model,模塊的職責相對混亂。在JSP Model1的基礎上,Govind Seshadri借鑑了MVC設計模式提出了JSP Model2模式(具體可查看文章Understanding JavaServer Pages Model 2 architecture),它的結構以下所示:
在JSP Model2中,Controller、View和Model分工明確,Model的數據變動,一般經過JavaBean修改View而後進行前端實時渲染,這樣從Web前端發起請求到數據回顯路線很是明確。不過這裏專門詢問了相應的後端開發人員,也可能經過JavaBean到Controller(Controller主要識別當前數據對應的JSP)再到JSP,所以在服務端MVC中,也可能產生這樣的流程View -> Controller -> Model -> Controller -> View。
在JSP Model2模式中,沒有作到先後端分離,前端的開發大大受到了限制。
對於Web前端開發而言,最直觀的感覺就是在Node服務中衍生Model2模式(例如結合Express以及EJS模板引擎等)。
在服務端的MVC模式設計中採用了HTTP協議通訊(HTTP是單工無狀態協議),所以View在不一樣的請求中都不保持狀態(狀態的保持須要額外經過Cookie存儲),而且經典MVC中Model經過觀察者模式告知View的環節被破壞(例如難以實現服務端推送)。固然在經典MVC中,Controller須要監聽View並對輸入作出反應,邏輯會變得很繁重,而在Model2中, Controller只關注路由處理等,而Model則更多的處理業務邏輯。
在上個世紀90年代,IBM旗下的子公司Taligent在用C/C++開發一個叫CommonPoint的圖形界面應用系統的時候提出了MVP的概念。
如上圖所示,MVP是MVC的模式的一種改良,打破了View對於Model的依賴,其他的依賴關係和MVC保持不變。
MVP模式可能產生的優缺點以下:
如上圖所示:MVVM模式是在MVP模式的基礎上進行了改良,將Presenter改良成ViewModel(抽象視圖):
能夠發現,MVVM在MVP的基礎上帶來了大量的好處,例如:
固然也帶來了一些額外的問題:
對前端開發而言MVVM是很是好的一種設計模式。在瀏覽器中,路由層能夠將控制權交由適當的ViewModel,後者又能夠更新並響應持續的View,而且經過一些小修改MVVM模式能夠很好的運行在服務器端,其中的緣由就在於Model與View已經徹底沒有了依賴關係(經過View與Model的去耦合,能夠容許短暫View與持續View的並存),這容許View經由給定的ViewModel進行渲染。
目前流行的框架Vue、React以及Angular都是MVVM設計模式的一種實現,而且均可以實現服務端渲染。須要注意目前的Web前端開發和傳統Model2須要模板引擎渲染的方式不一樣,經過Node啓動服務進行頁面渲染,而且經過代理的方式轉發請求後端數據,徹底能夠從後端的苦海中脫離,這樣一來也能夠大大的解放Web前端的生產力。
觀察者模式是使用一個subject目標對象維持一系列依賴於它的observer觀察者對象,將有關狀態的任何變動自動通知給這一系列觀察者對象。當subject目標對象須要告訴觀察者發生了什麼事情時,它會向觀察者對象們廣播一個通知。
如上圖所示:一個或多個觀察者對目標對象的狀態感興趣時,能夠將本身依附在目標對象上以便註冊感興趣的目標對象的狀態變化,目標對象的狀態發生改變就會發送一個通知消息,調用每一個觀察者的更新方法。若是觀察者對目標對象的狀態不感興趣,也能夠將本身從中分離。
發佈/訂閱模式使用一個事件通道,這個通道介於訂閱者和發佈者之間,該設計模式容許代碼定義應用程序的特定事件,這些事件能夠傳遞自定義參數,自定義參數包含訂閱者須要的信息,採用事件通道能夠避免發佈者和訂閱者之間產生依賴關係。
學生時期很長一段時間內用過Redis的發佈/訂閱機制,具體可查看zigbee-door/zigbee-tcp,可是慚愧的是沒有好好閱讀過這一塊的源碼。
觀察者模式:容許觀察者實例對象(訂閱者)執行適當的事件處理程序來註冊和接收目標實例對象(發佈者)發出的通知(即在觀察者實例對象上註冊update
方法),使訂閱者和發佈者之間產生了依賴關係,且沒有事件通道。不存在封裝約束的單一對象,目標對象和觀察者對象必須合做才能維持約束。 觀察者對象向訂閱它們的對象發佈其感興趣的事件。通訊只能是單向的。
發佈/訂閱模式:單一目標一般有不少觀察者,有時一個目標的觀察者是另外一個觀察者的目標。通訊能夠實現雙向。該模式存在不穩定性,發佈者沒法感知訂閱者的狀態。
這裏簡單的描述一下Vue的運行機制(須要注意分析的是 Runtime + Compiler 的 Vue.js)。
init
過程會初始化生命週期,初始化事件中心,初始化渲染、執行beforeCreate
周期函數、初始化 data
、props
、computed
、watcher
、執行created
周期函數等。$mount
方法對Vue實例進行掛載(掛載的核心過程包括模板編譯、渲染以及更新三個過程)。render
方法而是定義了template
,那麼須要經歷編譯階段。須要先將template
字符串編譯成 render function
,template
字符串編譯步驟以下 :
parse
正則解析template
字符串造成AST(抽象語法樹,是源代碼的抽象語法結構的樹狀表現形式)optimize
標記靜態節點跳過diff算法(diff算法是逐層進行比對,只有同層級的節點進行比對,所以時間的複雜度只有O(n)。若是對於時間複雜度不是很清晰的,能夠查看我寫的文章ziyi2/algorithms-javascript/漸進記號)generate
將AST轉化成render function
字符串render function
後,調用$mount
的mountComponent
方法,先執行beforeMount
鉤子函數,而後核心是實例化一個渲染Watcher
,在它的回調函數(初始化的時候執行,以及組件實例中監測到數據發生變化時執行)中調用updateComponent
方法(此方法調用render
方法生成虛擬Node,最終調用update
方法更新DOM)。render
方法將render function
渲染成虛擬的Node(真正的 DOM 元素是很是龐大的,由於瀏覽器的標準就把 DOM 設計的很是複雜。若是頻繁的去作 DOM 更新,會產生必定的性能問題,而 Virtual DOM 就是用一個原生的 JavaScript 對象去描述一個 DOM 節點,因此它比建立一個 DOM 的代價要小不少,並且修改屬性也很輕鬆,還能夠作到跨平臺兼容),render
方法的第一個參數是createElement
(或者說是h
函數),這個在官方文檔也有說明。update
方法,update
方法又會調用pacth
方法把虛擬DOM轉換成真正的DOM節點。須要注意在圖中忽略了新建真實DOM的狀況(若是沒有舊的虛擬Node,那麼能夠直接經過createElm
建立真實DOM節點),這裏重點分析在已有虛擬Node的狀況下,會經過sameVnode
判斷當前須要更新的Node節點是否和舊的Node節點相同(例如咱們設置的key
屬性發生了變化,那麼節點顯然不一樣),若是節點不一樣那麼將舊節點採用新節點替換便可,若是相同且存在子節點,須要調用patchVNode
方法執行diff算法更新DOM,從而提高DOM操做的性能。須要注意在初始化階段,沒有詳細描述數據的響應式過程,這個在響應式流程裏作說明。
init
的時候會利用Object.defineProperty
方法(不兼容IE8)監聽Vue實例的響應式數據的變化從而實現數據劫持能力(利用了JavaScript對象的訪問器屬性get
和set
,在將來的Vue3中會使用ES6的Proxy
來優化響應式原理)。在初始化流程中的編譯階段,當render function
被渲染的時候,會讀取Vue實例中和視圖相關的響應式數據,此時會觸發getter
函數進行依賴收集(將觀察者Watcher
對象存放到當前閉包的訂閱者Dep
的subs
中),此時的數據劫持功能和觀察者模式就實現了一個MVVM模式中的Binder,以後就是正常的渲染和更新流程。setter
函數,setter
會通知初始化依賴收集中的Dep
中的和視圖相應的Watcher
,告知須要從新渲染視圖,Wather
就會再次經過update
方法來更新視圖。能夠發現只要視圖中添加監聽事件,自動變動對應的數據變化時,就能夠實現數據和視圖的雙向綁定了。
瞭解了MV*設計模式、觀察者模式以及Vue運行機制以後,可能對於整個MVVM模式有了一個感性的認知,所以能夠來手動實現一下,這裏實現過程包括以下幾個步驟:
MVVM示例的使用以下所示,包括browser.js
(View視圖的更新)、mediator.js
(中介者)、binder.js
(MVVM的數據綁定引擎)、view.js
(視圖)、hijack.js
(數據劫持)以及mvvm.js
(MVVM實例)。本示例相關的代碼可查看github的ziyi2/mvvm:
<div id="app">
<input type="text" b-value="input.message" b-on-input="handlerInput">
<div>{{ input.message }}</div>
<div b-text="text"></div>
<div>{{ text }}</div>
<div b-html="htmlMessage"></div>
</div>
<script src="./browser.js"></script>
<script src="./mediator.js"></script>
<script src="./binder.js"></script>
<script src="./view.js"></script>
<script src="./hijack.js"></script>
<script src="./mvvm.js"></script>
<script>
let vm = new Mvvm({
el: '#app',
data: {
input: {
message: 'Hello Input!'
},
text: 'ziyi2',
htmlMessage: `<button>提交</button>`
},
methods: {
handlerInput(e) {
this.text = e.target.value
}
}
})
</script>
複製代碼
這裏簡單的描述一下MVVM實現的運行機制。
options
參數proxyData
將MVVM實例對象的data
數據代理到MVVM實例對象上Hijack
類實現數據劫持功能(對MVVM實例跟視圖對應的響應式數據進行監聽,這裏和Vue運行機制不一樣,幹掉了getter
依賴蒐集功能)b-value
、b-on-input
、b-html
等,實際上是Vue編譯的超級簡化版),update
方法對View解析綁定指令後的文檔碎片進行更新視圖處理Browser
採用了外觀模式對瀏覽器進行了簡單的兼容性處理setter
方法最簡單的中介者模式只須要實現發佈、訂閱和取消訂閱的功能。發佈和訂閱之間經過事件通道(channels)進行信息傳遞,能夠避免觀察者模式中產生依賴的狀況。中介者模式的代碼以下:
class Mediator {
constructor() {
this.channels = {}
this.uid = 0
}
/** * @Desc: 訂閱頻道 * @Parm: {String} channel 頻道 * {Function} cb 回調函數 */
sub(channel, cb) {
let { channels } = this
if(!channels[channel]) channels[channel] = []
this.uid ++
channels[channel].push({
context: this,
uid: this.uid,
cb
})
console.info('[mediator][sub] -> this.channels: ', this.channels)
return this.uid
}
/** * @Desc: 發佈頻道 * @Parm: {String} channel 頻道 * {Any} data 數據 */
pub(channel, data) {
console.info('[mediator][pub] -> chanel: ', channel)
let ch = this.channels[channel]
if(!ch) return false
let len = ch.length
// 後訂閱先觸發
while(len --) {
ch[len].cb.call(ch[len].context, data)
}
return this
}
/** * @Desc: 取消訂閱 * @Parm: {String} uid 訂閱標識 */
cancel(uid) {
let { channels } = this
for(let channel of Object.keys(channels)) {
let ch = channels[channel]
if(ch.length === 1 && ch[0].uid === uid) {
delete channels[channel]
console.info('[mediator][cancel][delete] -> chanel: ', channel)
console.info('[mediator][cancel] -> chanels: ', channels)
return
}
for(let i=0,len=ch.length; i<len; i++) {
if(ch[i].uid === uid) {
ch.splice(i,1)
console.info('[mediator][cancel][splice] -> chanel: ', channel)
console.info('[mediator][cancel] -> chanels: ', channels)
return
}
}
}
}
}
複製代碼
在每個MVVM實例中,都須要實例化一箇中介者實例對象,中介者實例對象的使用方法以下:
let mediator = new Mediator()
// 訂閱channel1
let channel1First = mediator.sub('channel1', (data) => {
console.info('[mediator][channel1First][callback] -> data', data)
})
// 再次訂閱channel1
let channel1Second = mediator.sub('channel1', (data) => {
console.info('[mediator][channel1Second][callback] -> data', data)
})
// 訂閱channel2
let channel2 = mediator.sub('channel2', (data) => {
console.info('[mediator][channel2][callback] -> data', data)
})
// 發佈(廣播)channel1,此時訂閱channel1的兩個回調函數會連續執行
mediator.pub('channel1', { name: 'ziyi1' })
// 發佈(廣播)channel2,此時訂閱channel2的回調函數執行
mediator.pub('channel2', { name: 'ziyi2' })
// 取消channel1標識爲channel1Second的訂閱
mediator.cancel(channel1Second)
// 此時只會執行channel1中標識爲channel1First的回調函數
mediator.pub('channel1', { name: 'ziyi1' })
複製代碼
對象的屬性可分爲數據屬性(特性包括[[Value]]
、[[Writable]]
、[[Enumerable]]
、[[Configurable]]
)和存儲器/訪問器屬性(特性包括[[ Get ]]
、[[ Set ]]
、[[Enumerable]]
、[[Configurable]]
),對象的屬性只能是數據屬性或訪問器屬性的其中一種,這些屬性的含義:
[[Configurable]]
: 表示可否經過 delete
刪除屬性從而從新定義屬性,可否修改屬性的特性,或者可否把屬性修改成訪問器屬性。[[Enumerable]]
: 對象屬性的可枚舉性。[[Value]]
: 屬性的值,讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值爲 undefined
。[[Writable]]
: 表示可否修改屬性的值。[[ Get ]]
: 在讀取屬性時調用的函數。默認值爲 undefined
。[[ Set ]]
: 在寫入屬性時調用的函數。默認值爲 undefined
。數據劫持就是使用了
[[ Get ]]
和[[ Set ]]
的特性,在訪問對象的屬性和寫入對象的屬性時可以自動觸發屬性特性的調用函數,從而作到監聽數據變化的目的。
對象的屬性能夠經過ES5的設置特性方法Object.defineProperty(data, key, descriptor)
改變屬性的特性,其中descriptor
傳入的就是以上所描述的特性集合。
let hijack = (data) => {
if(typeof data !== 'object') return
for(let key of Object.keys(data)) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.info('[hijack][get] -> val: ', val)
// 和執行 return data[key] 有什麼區別 ?
return val
},
set(newVal) {
if(newVal === val) return
console.info('[hijack][set] -> newVal: ', newVal)
val = newVal
// 若是新值是object, 則對其屬性劫持
hijack(newVal)
}
})
}
}
let person = { name: 'ziyi2', age: 1 }
hijack(person)
// [hijack][get] -> val: ziyi2
person.name
// [hijack][get] -> val: 1
person.age
// [hijack][set] -> newVal: ziyi
person.name = 'ziyi'
// 屬性類型變化劫持
// [hijack][get] -> val: { familyName:"ziyi2", givenName:"xiankang" }
person.name = { familyName: 'zhu', givenName: 'xiankang' }
// [hijack][get] -> val: ziyi2
person.name.familyName = 'ziyi2'
// 數據屬性
let job = { type: 'javascript' }
console.info(Object.getOwnPropertyDescriptor(job, "type"))
// 訪問器屬性
console.info(Object.getOwnPropertyDescriptor(person, "name"))
複製代碼
注意Vue3.0將不產用Object.defineProperty
方式進行數據監聽,緣由在於
hack
,因此若是要使數組響應化,須要注意使用Vue官方推薦的一些數組方法)在Vue3.0中將產用Proxy
解決以上痛點問題,固然會產生瀏覽器兼容性問題(例如萬惡的IE,具體可查看Can I use proxy)。
須要注意是的在
hijack
中只進行了一層屬性的遍歷,若是要作到對象深層次屬性的監聽,須要繼續對data[key]
進行hijack
操做,從而能夠達到屬性的深層次遍歷監聽,具體可查看mvvm/mvvm/hijack.js,
如上圖所示,數據雙向綁定主要包括數據的變化引發視圖的變化(Model -> 監聽數據變化 -> View)、視圖的變化又改變數據(View -> 用戶輸入監聽事件 -> Model),從而實現數據和視圖之間的強聯繫。
在實現了數據監聽的基礎上,加上用戶輸入事件以及視圖更新,就能夠簡單實現數據的雙向綁定(其實就是一個最簡單的Binder,只是這裏的代碼耦合嚴重):
<input id="input" type="text"> <div id="div"></div> 複製代碼
// 監聽數據變化
function hijack(data) {
if(typeof data !== 'object') return
for(let key of Object.keys(data)) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log('[hijack][get] -> val: ', val)
// 和執行 return data[key] 有什麼區別 ?
return val
},
set(newVal) {
if(newVal === val) return
console.log('[hijack][set] -> newVal: ', newVal)
val = newVal
// 更新全部和data.input數據相關聯的視圖
input.value = newVal
div.innerHTML = newVal
// 若是新值是object, 則對其屬性劫持
hijack(newVal)
}
})
}
}
let input = document.getElementById('input')
let div = document.getElementById('div')
// model
let data = { input: '' }
// 數據劫持
hijack(data)
// model -> view
data.input = '11111112221'
// view -> model
input.oninput = function(e) {
// model -> view
data.input = e.target.value
}
複製代碼
在MVVM的實現演示中,能夠發現使用了b-value
、b-text
、b-on-input
、b-html
等綁定屬性(這些屬性在該MVVM示例中自行定義的,並非html標籤原生的屬性,相似於vue的v-html
、v-model
、v-text
指令等),這些指令只是方便用戶進行Model和View的同步綁定操做而建立的,須要MVVM實例對象去識別這些指令並從新渲染出最終須要的DOM元素,例如
<div id="app">
<input type="text" b-value="message"> </div>
複製代碼
最終須要轉化成真實的DOM
<div id="app">
<input type="text" value='Hello World' /> </div>
複製代碼
那麼實現以上指令解析的步驟主要以下:
#app
元素#app
下的全部子元素)#app
元素HTML代碼以下:
<div id="app"> <input type="text" b-value="message" /> <input type="text" b-value="message" /> <input type="text" b-value="message" /> </div> <script src="./browser.js"></script> <script src="./binder.js"></script> <script src="./view.js"></script> 複製代碼
首先來看示例的使用
// 模型
let model = {
message: 'Hello World',
getData(key) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
val = val[keys[i]]
if(!val && i !== len - 1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`) }
}
return val
}
}
// 抽象視圖(實現功能將b-value中對應的model.message轉換成最終的value="Hello World")
new View('#app', model)
複製代碼
在view.js
中實現了#app
下的元素轉化成文檔碎片以及對全部子元素進行屬性遍歷操做(用於binder.js
的綁定屬性解析)
class View {
constructor(el, model) {
this.model = model
// 獲取須要處理的node節點
this.el = el.nodeType === Node.ELEMENT_NODE ? el : document.querySelector(el)
if(!this.el) return
// 將已有的el元素的全部子元素轉成文檔碎片
this.fragment = this.node2Fragment(this.el)
// 解析和處理綁定指令並修改文檔碎片
this.parseFragment(this.fragment)
// 將文檔碎片從新添加到dom樹
this.el.appendChild(this.fragment)
}
/** * @Desc: 將node節點轉爲文檔碎片 * @Parm: {Object} node Node節點 */
node2Fragment(node) {
let fragment = document.createDocumentFragment(),
child;
while(child = node.firstChild) {
// 給文檔碎片添加節點時,該節點會自動從dom中刪除
fragment.appendChild(child)
}
return fragment
}
/** * @Desc: 解析文檔碎片(在parseFragment中遍歷的屬性,須要在binder.parse中處理綁定指令的解析處理) * @Parm: {Object} fragment 文檔碎片 */
parseFragment(fragment) {
// 類數組轉化成數組進行遍歷
for(let node of [].slice.call(fragment.childNodes)) {
if(node.nodeType !== Node.ELEMENT_NODE) continue
// 綁定視圖指令解析
for(let attr of [].slice.call(node.attributes)) {
binder.parse(node, attr, this.model)
// 移除綁定屬性
node.removeAttribute(attr.name)
}
// 遍歷node節點樹
if(node.childNodes && node.childNodes.length) this.parseFragment(node)
}
}
}
複製代碼
接下來查看binder.js
如何處理綁定指令,這裏以b-value
的解析爲示例
(function(window, browser){
window.binder = {
/** * @Desc: 判斷是不是綁定屬性 * @Parm: {String} attr Node節點的屬性 */
is(attr) {
return attr.includes('b-')
},
/** * @Desc: 解析綁定指令 * @Parm: {Object} attr html屬性對象 * {Object} node Node節點 * {Object} model 數據 */
parse(node, attr, model) {
// 判斷是不是綁定指令,不是則不對該屬性進行處理
if(!this.is(attr.name)) return
// 獲取model數據
this.model = model
// b-value = 'message', 所以attr.value = 'message'
let bindValue = attr.value,
// 'b-value'.substring(2) = value
bindType = attr.name.substring(2)
// 綁定視圖指令b-value處理
// 這裏採用了命令模式
this[bindType](node, bindValue.trim())
},
/** * @Desc: 值綁定處理(b-value) * @Parm: {Object} node Node節點 * {String} key model的屬性 */
value(node, key) {
this.update(node, key)
},
/** * @Desc: 值綁定更新(b-value) * @Parm: {Object} node Node節點 * {String} key model的屬性 */
update(node, key) {
// this.model.getData是用於獲取model對象的屬性值
// 例如 model = { a : { b : 111 } }
// <input type="text" b-value="a.b" />
// this.model.getData('a.b') = 111
// 從而能夠將input元素更新爲<input type="text" value="111" />
browser.val(node, this.model.getData(key))
}
}
})(window, browser)
複製代碼
在browser.js
中使用外觀模式對瀏覽器原生的事件以及DOM操做進行了再封裝,從而能夠作到瀏覽器的兼容處理等,這裏只對b-value
須要的DOM操做進行了封裝處理,方便閱讀
let browser = {
/** * @Desc: Node節點的value處理 * @Parm: {Object} node Node節點 * {String} val 節點的值 */
val(node, val) {
// 將b-value轉化成value,須要注意的是解析完後在view.js中會將b-value屬性移除
node.value = val || ''
console.info(`[browser][val] -> node: `, node)
console.info(`[browser][val] -> val: `, val)
}
}
複製代碼
至此MVVM示例中簡化的Model -> ViewModel (未實現數據監聽功能)-> View路走通,能夠查看視圖綁定指令的解析的demo。
ViewModel(內部綁定器Binder)的做用不只僅是實現了Model到View的自動同步(Sync Logic)邏輯(以上視圖綁定指令的解析的實現只是實現了一個視圖的綁定指令初始化,一旦Model變化,視圖要更新的功能並無實現),還實現了View到Model的自動同步邏輯,從而最終實現了數據的雙向綁定。
所以只要在視圖綁定指令的解析的基礎上增長Model的數據監聽功能(數據變化更新視圖)和View視圖的input
事件監聽功能(監聽視圖從而更新相應的Model數據,注意Model的變化又會由於數據監遵從而更新和Model相關的視圖)就能夠實現View和Model的雙向綁定。同時須要注意的是,數據變化更新視圖的過程須要使用發佈/訂閱模式,若是對流程不清晰,能夠繼續回看MVVM的結構設計。
在簡易視圖指令的編譯過程實現的基礎上進行修改,首先是HTML代碼
<div id="app"> <input type="text" id="input1" b-value="message"> <input type="text" id="input2" b-value="message"> <input type="text" id="input3" b-value="message"> </div> <!-- 新增中介者 --> <script src="./mediator.js"></script> <!-- 新增數據劫持 --> <script src="./hijack.js"></script> <script src="./view.js"></script> <script src="./browser.js"></script> <script src="./binder.js"></script> 複製代碼
mediator.js
再也不敘述,具體回看中介者模式的實現,view.js
和browser.js
也再也不敘述,具體回看簡易視圖指令的編譯過程實現。
示例的使用:
// 模型
let model = {
message: 'Hello World',
setData(key, newVal) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
if(i < len - 1) {
val = val[keys[i]]
} else {
val[keys[i]] = newVal
}
}
// console.log('[mvvm][setData] -> val: ', val)
},
getData(key) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
val = val[keys[i]]
if(!val && i !== len - 1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`) }
}
return val
}
}
// 發佈/訂閱對象
let mediator = new Mediator()
// 數據劫持(監聽model的變化,併發布model數據變化消息)
hijack(model, mediator)
// 抽象視圖(實現綁定指令的解析,並訂閱model數據的變化從而更新視圖)
new View('#app', model, mediator)
// model -> view (會觸發數據劫持的set函數,從而發佈model變化,在binder中訂閱model數據變化後會更新視圖)
model.message = 'Hello Ziyi233333222'
複製代碼
首先看下數據劫持,在** 數據劫持的實現的基礎上,增長了中介者對象的發佈數據變化功能(在抽象視圖的Binder**中會訂閱這個數據變化)
var hijack = (function() {
class Hijack {
/** * @Desc: 數據劫持構造函數 * @Parm: {Object} model 數據 * {Object} mediator 發佈訂閱對象 */
constructor(model, mediator) {
this.model = model
this.mediator = mediator
}
/** * @Desc: model數據劫持 * @Parm: * */
hijackData() {
let { model, mediator } = this
for(let key of Object.keys(model)) {
let val = model[key]
Object.defineProperty(model, key, {
enumerable: true,
configurable: false,
get() {
return val
},
set(newVal) {
if(newVal === val) return
val = newVal
// 發佈數據劫持的數據變化信息
console.log('[mediator][pub] -> key: ', key)
// 重點注意這裏的通道,在最後的MVVM示例中和這裏的實現不同
mediator.pub(key)
}
})
}
}
}
return (model, mediator) => {
if(!model || typeof model !== 'object') return
new Hijack(model, mediator).hijackData()
}
})()
複製代碼
接着重點來看binder.js
中的實現
(function(window, browser){
window.binder = {
/** * @Desc: 判斷是不是綁定屬性 * @Parm: {String} attr Node節點的屬性 */
is(attr) {
return attr.includes('b-')
},
/** * @Desc: 解析綁定指令 * @Parm: {Object} attr html屬性對象 * {Object} node Node節點 * {Object} model 數據 * {Object} mediator 中介者 */
parse(node, attr, model, mediator) {
if(!this.is(attr.name)) return
this.model = model
this.mediator = mediator
let bindValue = attr.value,
bindType = attr.name.substring(2)
// 綁定視圖指令處理
this[bindType](node, bindValue.trim())
},
/** * @Desc: 值綁定處理(b-value) * @Parm: {Object} node Node節點 * {String} key model的屬性 */
value(node, key) {
this.update(node, key)
// View -> ViewModel -> Model
// 監聽用戶的輸入事件
browser.event.add(node, 'input', (e) => {
// 更新model
let newVal = browser.event.target(e).value
// 設置對應的model數據(由於進行了hijack(model))
// 由於進行了hijack(model),對model進行了變化監聽,所以會觸發hijack中的set,從而觸發set中的mediator.pub
this.model.setData(key, newVal)
})
// 一旦model變化,數據劫持會mediator.pub變化的數據
// 訂閱數據變化更新視圖(閉包)
this.mediator.sub(key, () => {
console.log('[mediator][sub] -> key: ', key)
console.log('[mediator][sub] -> node: ', node)
this.update(node, key)
})
},
/** * @Desc: 值綁定更新(b-value) * @Parm: {Object} node Node節點 * {String} key model的屬性 */
update(node, key) {
browser.val(node, this.model.getData(key))
}
}
})(window, browser)
複製代碼
最終實現了具備viewModel的MVVM簡單實例,具體查看ViewModel的實現的demo。
在ViewModel的實現的基礎上:
b-text
、b-html
、b-on-*
(事件監聽)指令的解析hijack.js
實現了對Model數據的深層次監聽hijack.js
中的發佈和訂閱的channel
採用HTML屬性中綁定的指令對應的值進行處理(例如b-value="a.b.c.d"
,那麼channel
就是'a.b.c.d'
,這裏是將Vue的觀察者模式改爲中介者模式後的一種嘗試,只是一種實現方式,固然採用觀察者模式關聯性更強,而採用中介者模式會更解耦)。browser.js
中新增了事件監聽的兼容處理、b-html
和b-text
等指令的DOM操做api等因爲篇幅太長了,這裏就不過多作說明了,感興趣的童鞋能夠直接查看ziyi2/mvvm,須要注意該示例中還存在必定的缺陷,例如Model的屬性是一個對象,且該對象被重寫時,發佈和訂閱維護的channels
中未將舊的屬性監聽的channel
移除處理。