vue是採用數據劫持配合發佈者訂閱模式的方式,經過Object.defineProperty()來劫持各個屬性setter,getter。在數據發生變化時,發佈消息給依賴收集器(dep),去通知觀察者(watcher),作出對應的回調函數,去更新視圖。從而實現數據驅動視圖。html
MVVM做爲綁定的入口,整合Observer,Compile和Watcher三者,經過Observer來監聽model數據變化,經過Compile來解析編譯模板指令,最終利用Watcher搭起Observer,Compile之間的通訊橋樑,實現了數據變化=》視圖更新,視圖交互變化=》數據model變動的雙向綁定結果vue
簡單功能介紹:node
1.數據改變,視圖自動更新git
2.視圖交互變化,數據變動github
3.解析v-text,v-html指令數組
4.綁定v-on和@的事件bash
5.數據代理markdown
如圖所示,MVVM要實現劫持監聽全部屬性和解析指令,同時也要實現數據代理功能app
咱們知道頻繁的操做dom是很是消耗性能的,因此咱們須要將真實dom移入內存中操做--文檔碎片。因此Compile主要作了一下幾件事:dom
'''
class Compile { constructor(el, vm) { // 判斷el類型,獲得dom對象 this.el = this.isElementNode(el) ? el : document.querySelector(el) // 保存MVVM實例對象 this.vm = vm // 獲取文檔碎片對象,存放內存中減小重繪和迴流 const fragment = this.node2Frament(this.el) // 編譯模板 this.compile(fragment) // 添加到根元素上 this.el.appendChild(fragment) } // 判斷元素節點 isElementNode (node) { return node.nodeType === 1 } // 文檔碎片對象 node2Frament (el) { // 建立文檔碎片對象 const f = document.createDocumentFragment() let firstChild // 將dom對象中的節點以此添加到f文檔碎片對象 while (firstChild = el.firstChild) { f.appendChild(firstChild) } return f } // 編譯模板 compile (fragment) { // 獲取子節點 var childNodes = fragment.childNodes // 轉換數組遍歷 childNodes = [...childNodes] childNodes.forEach((child) => { // 元素節點 if (child.nodeType === 1) { // 處理元素節點 this.compileElement(child) } else { // 處理文本文本節點 this.compileText(child) } // 元素節點是否有子節點 if (child.childNodes && child.childNodes.length) { // 遞歸 this.compile(child) } }) } // 處理文本節點 compileText (node) { // 獲取文本信息 var content = node.textContent const rgb = /\{\{(.+?)\}\}/ // 若是有{{}}表達式 if (rgb.test(content)) { // 編譯內容 compileUtil['text'](node, content, this.vm) } } // 處理元素節點 compileElement (node) { // 獲取元素上屬性 var attributes = node.attributes attributes = [...attributes] // 遍歷屬性 attributes.forEach((attr) => { // 解構賦值獲取屬性名屬性值 const { name, value } = attr; // 判斷屬性名是否v-開頭 if (this.isDirective(name)) { // 分割字符串 const [, dirctive] = name.split('-') // dirname 爲html text等,eventName爲事件名 const [dirName, eventName] = dirctive.split(':') // 跟新數據 數據驅動視圖 compileUtil[dirName](node, value, this.vm, eventName) // 刪除標籤上的屬性 node.removeAttribute('v-' + dirctive) } else if (this.isEventive(name)) { //@開頭的事件 let [, eventName] = name.split('@') compileUtil['on'](node, value, this.vm, eventName) node.removeAttribute('@' + eventName) } }) } isDirective (attrName) { return attrName.startsWith('v-') } isEventive (eventName) { return eventName.startsWith('@') }}
'''複製代碼
經過Compile類解析了節點及文本各個指令和{{}}表達式,在經過編譯工具數據與視圖結合
``` class Compile { constructor(el, vm) { // 判斷el類型,獲得dom對象 this.el = this.isElementNode(el) ? el : document.querySelector(el) // 保存MVVM實例對象 this.vm = vm // 獲取文檔碎片對象,存放內存中減小重繪和迴流 const fragment = this.node2Frament(this.el) // 編譯模板 this.compile(fragment) // 添加到根元素上 this.el.appendChild(fragment) } // 判斷元素節點 isElementNode (node) { return node.nodeType === 1 } // 文檔碎片對象 node2Frament (el) { // 建立文檔碎片對象 const f = document.createDocumentFragment() let firstChild // 將dom對象中的節點以此添加到f文檔碎片對象 while (firstChild = el.firstChild) { f.appendChild(firstChild) } return f } // 編譯模板 compile (fragment) { // 獲取子節點 var childNodes = fragment.childNodes // 轉換數組遍歷 childNodes = [...childNodes] childNodes.forEach((child) => { // 元素節點 if (child.nodeType === 1) { // 處理元素節點 this.compileElement(child) } else { // 處理文本文本節點 this.compileText(child) } // 元素節點是否有子節點 if (child.childNodes && child.childNodes.length) { // 遞歸 this.compile(child) } }) } // 處理文本節點 compileText (node) { // 獲取文本信息 var content = node.textContent const rgb = /\{\{(.+?)\}\}/ // 若是有{{}}表達式 if (rgb.test(content)) { // 編譯內容 compileUtil['text'](node, content, this.vm) } } // 處理元素節點 compileElement (node) { // 獲取元素上屬性 var attributes = node.attributes attributes = [...attributes] // 遍歷屬性 attributes.forEach((attr) => { // 解構賦值獲取屬性名屬性值 const { name, value } = attr; // 判斷屬性名是否v-開頭 if (this.isDirective(name)) { // 分割字符串 const [, dirctive] = name.split('-') // dirname 爲html text等,eventName爲事件名 const [dirName, eventName] = dirctive.split(':') // 跟新數據 數據驅動視圖 compileUtil[dirName](node, value, this.vm, eventName) // 刪除標籤上的屬性 node.removeAttribute('v-' + dirctive) } else if (this.isEventive(name)) { //@開頭的事件 let [, eventName] = name.split('@') compileUtil['on'](node, value, this.vm, eventName) node.removeAttribute('@' + eventName) } }) } isDirective (attrName) { return attrName.startsWith('v-') } isEventive (eventName) { return eventName.startsWith('@') }} ```複製代碼
```
const updater = { textUpdater (node, value) { node.textContent = value }, htmlUpdater (node, value) { node.innerHTML = value }, modelUpdater (node, value) { node.value = value }}
```複製代碼
經過Compile及兩個工具對象完全將html上模板中的{{}}及指令轉換爲所須要的數據,完成了視圖上解析指令--初始化視圖
經過Object,defineProperty的方法來劫持每一個屬性,當屬性被修改時,會觸發set函數,此時,咱們要通知變化屬性的每一個訂閱者(watcher)來改變視圖在劫持每一個屬性的時候。
那每一個訂閱者又存在哪裏呢,這時候咱們須要一個dep實例來收集他們。
```
let uid = 0class Dep { constructor() { this.id = uid++ this.subs = [] } // 收集觀察者 addSub (watcher) { this.subs.push(watcher) } // 通知觀察者 notify () { this.subs.forEach(w => w.update()) }}
```複製代碼
每一個dep實例都有一個數組來存放watcher,當數據發生改變,會觸發每一個實例的notify方法,watcher再去更新視圖,達到了響應式。
```
defineReactive: function(data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { if (Dep.target) { dep.depend(); } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; // 新的值是object的話,進行監聽 childObj = observe(newVal); // 通知訂閱者 dep.notify(); } }); }```複製代碼
那麼watcher又是怎麼存放到subs數組中的呢?
在Compile函數中,咱們每次實現解析操做的時候,咱們添加一個watcher實例
```
class watcher { constructor(vm, expr, cb) { this.vm = vm this.expr = expr this.cb = cb this.oldVal = this.getOldVal() } // 獲取舊值 getOldVal () { Dep.target = this const oldVal = compileUtil.getVal(this.expr, this.vm) Dep.target = null return oldVal } // 更新視圖 update () { const newVal = compileUtil.getVal(this.expr, this.vm) if (this.oldVal !== newVal) { this.cb(newVal) } }}```複製代碼
在watcher實例裏面綁定了一個更新函數cb,當調用update時候,就去執行更新函數到達渲染。
在初始模板的時候,在Compile裏,每一個watcher獲取oldVal值時,會觸發Object.defineProperty中get的方法,再觸發get函數前,執行Dep.target=this,將watcher實例綁定到Dep.target,當咱們執行get方法時,調用dep.addSub(Dep.target),就將watcher實例添加到了dep中的數組裏了。當模板渲染完成時。模板裏每一個須要解析的數據都綁定了一個watcher,同時,每一個watcher都被添加到了對應的dep中。
整個流程概述:
項目github地址
https://github.com/6sy/vue-mvvm.git複製代碼
經過本身實現一下響應式原理,發現了本身還有不少須要走的路,在這項目中,是沒法監聽到數組變化的,而在vue當中,經過對源碼的瞭解,是經過添加攔截器的方式,在數組和原型對象之間添加,改寫了數組的七個方法,從而達到了響應式。
以上若是有什麼錯誤以及不足的地方,請大佬們熱心指出來。讓咱們一塊兒進步。