56

对Vue中的MVVM原理解析和实现

 4 years ago
source link: http://www.cnblogs.com/dingxingxing/p/13296409.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

对Vue中的MVVM原理解析和实现

首先你对Vue需要有一定的了解,知道MVVM。这样才能更有助于你顺利的完成下面原理的阅读学习和编写

下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到:

1.Vue数据双向绑定核心代码模块以及实现原理

2.订阅者-发布者模式是如何做到让数据驱动视图、视图驱动数据再驱动视图

3.如何对元素节点上的指令进行解析并且关联订阅者实现视图更新

1、思路整理

实现的流程图:

2eEFzez.png!web

我们要实现一个类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打桩尝试,还请不要吝啬一个小星星哟!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK