1. 程式人生 > >對Vue中的MVVM原理解析和實現

對Vue中的MVVM原理解析和實現

對Vue中的MVVM原理解析和實現

首先你對Vue需要有一定的瞭解,知道MVVM。這樣才能更有助於你順利的完成下面原理的閱讀學習和編寫

 

下面由我阿巴阿巴的詳細走一遍Vue中MVVM原理的實現,這篇文章大家可以學習到:

1.Vue資料雙向繫結核心程式碼模組以及實現原理

2.訂閱者-釋出者模式是如何做到讓資料驅動檢視、檢視驅動資料再驅動檢視

3.如何對元素節點上的指令進行解析並且關聯訂閱者實現檢視更新

 

1、思路整理

實現的流程圖:

 

我們要實現一個類MVVM簡單版本的Vue框架,就需要實現一下幾點:

1、實現一個數據監聽Observer,對資料物件的所有屬性進行監聽,資料發生變化可以獲取到最新值通知訂閱者。

2、實現一個解析器Compile解析頁面節點指令,初始化檢視。

3、實現一個觀察者Watcher,訂閱資料變化同時繫結相關更新函式。並且將自己放入觀察者集合Dep中。Dep是Observer和Watcher的橋樑,資料改變通知到Dep,然後Dep通知相應的Watcher去更新檢視。

 

2、實現

以下采用ES6的寫法,比較簡潔,所以大概在300多行程式碼實現了一個簡單的MVVM框架。

1、實現html頁面

按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>

 

2、實現解析器和觀察者

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 }

 

3、實現資料劫持監聽

我們有了資料監聽,還需要一個觀察者可以觸發更新檢視。因為需要資料改變才能觸發更新,所有還需要一個橋樑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 }

 




3、總結

其實複雜的地方有三點:

1、指令解析的各種操作有點複雜饒人,其中包含DOM的基本操作和一些ES中的API使用。但是你靜下心去讀去想,肯定是能理順的。

2、資料劫持中Dep的理解,一是收集觀察者的集合,二是連線Observer和watcher的橋樑。

3、觀察者是什麼時候進行繫結的?又是如何工作實現了資料驅動檢視,檢視驅動資料驅動檢視的。

 

 

在gitHub上有上述原始碼地址,歡迎clone打樁嘗試,還請不要吝嗇一個小星星