首先你對Vue須要有必定的瞭解,知道MVVM。這樣才能更有助於你順利的完成下面原理的閱讀學習和編寫
下面由我阿巴阿巴的詳細走一遍Vue中MVVM原理的實現,這篇文章你們能夠學習到:html
1.Vue數據雙向綁定核心代碼模塊以及實現原理vue
2.訂閱者-發佈者模式是如何作到讓數據驅動視圖、視圖驅動數據再驅動視圖node
3.如何對元素節點上的指令進行解析而且關聯訂閱者實現視圖更新git
實現的流程圖:github
咱們要實現一個類MVVM簡單版本的Vue框架,就須要實現一下幾點:app
一、實現一個數據監聽Observer,對數據對象的全部屬性進行監聽,數據發生變化能夠獲取到最新值通知訂閱者。框架
二、實現一個解析器Compile解析頁面節點指令,初始化視圖。函數
三、實現一個觀察者Watcher,訂閱數據變化同時綁定相關更新函數。而且將本身放入觀察者集合Dep中。Dep是Observer和Watcher的橋樑,數據改變通知到Dep,而後Dep通知相應的Watcher去更新視圖。學習
如下采用ES6的寫法,比較簡潔,因此大概在300多行代碼實現了一個簡單的MVVM框架。this
按Vue的寫法在頁面定義好一些數據跟指令,引入了兩個JS文件。先實例化一個MVue的對象,傳入咱們的el,data,methods這些參數。待會再看Mvue.js文件是什麼?
html
1 <body> 2 <div id="app"> 3 <h2>{{person.name}} --- {{person.age}}</h2> 4 <h3>{{person.fav}}</h3> 5 <h3>{{person.a.b}}</h3> 6 <ul> 7 <li>1</li> 8 <li>2</li> 9 <li>3</li> 10 </ul> 11 <h3>{{msg}}</h3> 12 <div v-text="msg"></div> 13 <div v-text="person.fav"></div> 14 <div v-html="htmlStr"></div> 15 <input type="text" v-model="msg"> 16 <button v-on:click="click111">按鈕on</button> 17 <button @click="click111">按鈕@</button> 18 </div> 19 <script src="./MVue.js"></script> 20 <script src="./Observer.js"></script> 21 <script> 22 let vm = new MVue({ 23 el: '#app', 24 data: { 25 person: { 26 name: '星哥', 27 age: 18, 28 fav: '姑娘', 29 a: { 30 b: '787878' 31 } 32 }, 33 msg: '學習MVVM實現原理', 34 htmlStr: '<h4>你們學的怎麼樣</h4>', 35 }, 36 methods: { 37 click111() { 38 console.log(this) 39 this.person.name = '學習MVVM' 40 // this.$data.person.name = '學習MVVM' 41 } 42 } 43 }) 44 </script> 45 46 </body>
MVue.js
1 // 先建立一個MVue類,它是一個入口 2 Class MVue { 3 construction(options) { 4 this.$el = options.el 5 this.$data = options.data 6 this.$options = options 7 } 8 if(this.$el) { 9 // 1.實現一個數據的觀察者 --先看解析器,再看Obeserver 10 new Observer(this.$data) 11 // 2.實現一個指令解析器 12 new Compile(this.$el,this) 13 } 14 } 15 16 // 定義一個Compile類解析元素節點和指令 17 class Compile { 18 constructor(el,vm) { 19 // 判斷el是不是元素節點對象,不是就經過DOM獲取 20 this.el = this.isElementNode(el) ? el : document.querySelector(el) 21 this.vm = vm 22 // 1.獲取文檔碎片對象,放入內存中能夠減小頁面的迴流和重繪 23 const fragment = this.node2Fragment(this.el) 24 25 // 2.編輯模板 26 this.compile(fragment) 27 28 // 3.追加子元素到根元素(還原頁面) 29 this.el.appendChild(fragment) 30 } 31 32 // 將元素插入到文檔碎片中 33 node2Fragment(el) { 34 const f = document.createDocumnetFragment(); 35 let firstChild 36 while(firstChild = el.firstChild) { 37 // appendChild 38 // 將已經存在的節點再次插入,那麼原來位置的節點自動刪除,並在新的位置從新插入。 39 f.appendChild(firstChild) 40 } 41 // 此處執行完,頁面已經沒有元素節點了 42 return f 43 } 44 45 // 解析模板 46 compile(frafment) { 47 // 1.獲取子節點 48 conts childNodes = fragment.childNodes; 49 [...childNodes].forEach(child => { 50 if(this.isElementNode(child)) { 51 // 是元素節點 52 // 編譯元素節點 53 this.compileElement(child) 54 } else { 55 // 文本節點 56 // 編譯文本節點 57 this.compileText(child) 58 } 59 60 // 嵌套子節點進行遍歷解析 61 if(child.childNodes && child.childNodes.length) { 62 this.compule(child) 63 } 64 }) 65 } 66 67 // 判斷是元素節點仍是屬性節點 68 isElementNode(node) { 69 // nodeType屬性返回 以數字值返回指定節點的節點類型。1-元素節點 2-屬性節點 70 return node.nodeType === 1 71 } 72 73 // 編譯元素節點 74 compileElement(node) { 75 // 得到元素屬性集合 76 const attributes = node.attributes 77 [...attributes].forEach(attr => { 78 const {name, value} = attr 79 if(this.isDirective(name)) { // 判斷屬性是否是以v-開頭的指令 80 // 解析指令(v-mode v-text v-on:click 等...) 81 const [, dirctive] = name.split('-') 82 const [dirName, eventName] = dirctive.split(':') 83 // 初始化視圖 將數據渲染到視圖上 84 compileUtil[dirName](node, value, this.vm, eventName) 85 86 // 刪除有指令的標籤上的屬性 87 node.removeAttribute('v-' + dirctive) 88 } else if (this.isEventName(name)) { //判斷屬性是否是以@開頭的指令 89 // 解析指令 90 let [, eventName] = name.split('@') 91 compileUtil['on'](node,val,this.vm, eventName) 92 93 // 刪除有指令的標籤上的屬性 94 node.removeAttribute('@' + eventName) 95 } else if(this.isBindName(name)) { //判斷屬性是否是以:開頭的指令 96 // 解析指令 97 let [, attrName] = name.split(':') 98 compileUtil['bind'](node,val,this.vm, attrName) 99 100 // 刪除有指令的標籤上的屬性 101 node.removeAttribute(':' + attrName) 102 } 103 }) 104 } 105 106 // 編譯文本節點 107 compileText(node) { 108 const content = node.textContent 109 if(/\{\{(.+?)\}\}/.test(content)) { 110 compileUtil['text'](node, content, this.vm) 111 } 112 } 113 114 // 判斷屬性是否是指令 115 isDirective(attrName) { 116 return attrName.startsWith('v-') 117 } 118 // 判斷屬性是否是以@開頭的事件指令 119 isEventName(attrName) { 120 return attrName.startsWith('@') 121 } 122 // 判斷屬性是否是以:開頭的事件指令 123 isBindName(attrName) { 124 return attrName.startsWith(':') 125 } 126 } 127 128 129 // 定義一個對象,針對不一樣指令執行不一樣操做 130 const compileUtil = { 131 // 解析參數(包含嵌套參數解析),獲取其對應的值 132 getVal(expre, vm) { 133 return expre.split('.').reduce((data, currentVal) => { 134 return data[currentVal] 135 }, vm.$data) 136 }, 137 // 獲取當前節點內參數對應的值 138 getgetContentVal(expre,vm) { 139 return expre.replace(/\{\{(.+?)\}\}/g, (...arges) => { 140 return this.getVal(arges[1], vm) 141 }) 142 }, 143 // 設置新值 144 setVal(expre, vm, inputVal) { 145 return expre.split('.').reduce((data, currentVal) => { 146 return data[currentVal] = inputVal 147 }, vm.$data) 148 }, 149 150 // 指令解析:v-test 151 test(node, expre, vm) { 152 let value; 153 if(expre.indexOf('{{') !== -1) { 154 // 正則匹配{{}}裏的內容 155 value = expre.replace(/\{\{(.+?)\}\}/g, (...arges) => { 156 157 // new watcher這裏相關的先能夠不看,等後面講解寫到觀察者再回頭看。這裏是綁定觀察者實現 的效果是經過改變數據會觸發視圖,即數據=》視圖。 158 // 沒有new watcher 不影響視圖初始化(頁面參數的替換渲染)。 159 // 訂閱數據變化,綁定更新函數。 160 new watcher(vm, arges[1], () => { 161 // 確保 {{person.name}}----{{person.fav}} 不會由於一個參數變化都被成新值 162 this.updater.textUpdater(node, this.getgetContentVal(expre,vm)) 163 }) 164 165 return this.getVal(arges[1],vm) 166 }) 167 } else { 168 // 同上,先不看 169 // 數據=》視圖 170 new watcher(vm, expre, (newVal) => { 171 // 找不到{}說明是test指令,因此當前節點只有一個參數變化,直接用回調函數傳入的新值 172 this.updater.textUpdater(node, newVal) 173 }) 174 175 value = this.getVal(expre,vm) 176 } 177 178 // 將數據替換,更新到視圖上 179 this.updater.textUpdater(node,value) 180 }, 181 //指令解析: v-html 182 html(node, expre, vm) { 183 const value = this.getVal(expre, vm) 184 185 // 同上,先不看 186 // 綁定觀察者 數據=》視圖 187 new watcher(vm, expre (newVal) => { 188 this.updater.htmlUpdater(node, newVal) 189 }) 190 191 // 將數據替換,更新到視圖上 192 this.updater.htmlUpdater(node, newVal) 193 }, 194 // 指令解析:v-mode 195 model(node,expre, vm) { 196 const value = this.getVal(expre, vm) 197 198 // 同上,先不看 199 // 綁定觀察者 數據=》視圖 200 new watcher(vm, expre, (newVal) => { 201 this.updater.modelUpdater(node, newVal) 202 }) 203 204 // input框 視圖=》數據=》視圖 205 node.addEventListener('input', (e) => { 206 //設置新值 - 將input值賦值到v-model綁定的參數上 207 this.setVal(expre, vm, e.traget.value) 208 }) 209 // 將數據替換,更新到視圖上 210 this.updater.modelUpdater(node, value) 211 }, 212 // 指令解析: v-on 213 on(node, expre, vm, eventName) { 214 // 或者指令綁定的事件函數 215 let fn = vm.$option.methods && vm.$options.methods[expre] 216 // 監聽函數並調用 217 node.addEventListener(eventName,fn.bind(vm),false) 218 }, 219 // 指令解析: v-bind 220 bind(node, expre, vm, attrName) { 221 const value = this.getVal(expre,vm) 222 this.updater.bindUpdate(node, attrName, value) 223 } 224 225 // updater對象,管理不一樣指令對應的更新方法 226 updater: { 227 // v-text指令對應更新方法 228 textUpdater(node, value) { 229 node.textContent = value 230 }, 231 // v-html指令對應更新方法 232 htmlUpdater(node, value) { 233 node.innerHTML = value 234 }, 235 // v-model指令對應更新方法 236 modelUpdater(node,value) { 237 node.value = value 238 }, 239 // v-bind指令對應更新方法 240 bindUpdate(node, attrName, value) { 241 node[attrName] = value 242 } 243 }, 244 }
咱們有了數據監聽,還須要一個觀察者能夠觸發更新視圖。由於須要數據改變才能觸發更新,全部還須要一個橋樑Dep收集全部觀察者(觀察者集合),鏈接Observer和Watcher。數據改變通知Dep,Dep通知相應的觀察者進行視圖更新。
Observer.js
1 // 定義一個觀察者 2 class watcher { 3 constructor(vm, expre, cb) { 4 this.vm = vm 5 this.expre = expre 6 this.cb =cb 7 // 把舊值保存起來 8 this.oldVal = this.getOldVal() 9 } 10 // 獲取舊值 11 getOldVal() { 12 // 將watcher放到targe值中 13 Dep.target = this 14 // 獲取舊值 15 const oldVal = compileUtil.getVal(this.expre, this.vm) 16 // 將target值清空 17 Dep.target = null 18 return oldVal 19 } 20 // 更新函數 21 update() { 22 const newVal = compileUtil.getVal(this.expre, this.vm) 23 if(newVal !== this.oldVal) { 24 this.cb(newVal) 25 } 26 } 27 } 28 29 30 // 定義一個觀察者集合 31 class Dep { 32 constructor() { 33 this.subs = [] 34 } 35 // 收集觀察者 36 addSub(watcher) { 37 this.subs.push(watcher) 38 } 39 //通知觀察者去更新 40 notify() { 41 this.subs.forEach(w => w.update()) 42 } 43 } 44 45 46 47 // 定義一個Observer類經過gettr,setter實現數據的監聽綁定 48 class Observer { 49 constructor(data) { 50 this.observer(data) 51 } 52 53 // 定義函數解析data,實現數據劫持 54 observer (data) { 55 if(data && typeof data === 'object') { 56 // 是對象遍歷對象寫入getter,setter方法 57 Reflect.ownKeys(data).forEach(key => { 58 this.defineReactive(data, key, data[key]); 59 }) 60 } 61 } 62 63 // 數據劫持方法 64 defineReactive(obj,key, value) { 65 // 遞歸遍歷 66 this.observer(data) 67 // 實例化一個dep對象 68 const dep = new Dep() 69 // 經過ES5的API實現數據劫持 70 Object.defineProperty(obj, key, { 71 enumerable: true, 72 configurable: false, 73 get() { 74 // 當讀當前值的時候,會觸發。 75 // 訂閱數據變化時,往Dep中添加觀察者 76 Dep.target && dep.addSub(Dep.target) 77 return value 78 }, 79 set: (newValue) => { 80 // 對新數據進行劫持監聽 81 this.observer(newValue) 82 if(newValue !== value) { 83 value = newValue 84 } 85 // 告訴dep通知變化 86 dep.notify() 87 } 88 }) 89 } 90 91 }