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

栏目: IT技术 · 发布时间: 4年前

内容简介:下面由我阿巴阿巴的详细走一遍Vue中MVVM原理的实现,这篇文章大家可以学习到:1.Vue数据双向绑定核心代码模块以及实现原理2.订阅者-发布者模式是如何做到让数据驱动视图、视图驱动数据再驱动视图

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

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

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

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

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

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

1、思路整理

实现的流程图:

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

我们要实现一个类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 Face 4: 交互设计精髓

About Face 4: 交互设计精髓

[美] 艾伦·库伯、[美] 罗伯特·莱曼、[美] 戴维·克罗宁、[美] 克里斯托弗·诺埃塞尔 / 倪卫国、刘松涛、杭敏、薛菲 / 电子工出版社 / 2015-10 / 118.00元

《About Face 4: 交互设计精髓》是《About Face 3:交互设计精髓》的升级版,此次升级把全书的结构重组优化,更加精练和易用;更新了一些适合当下时代的术语和实例,文字全部重新编译,更加清晰易读;增加了更多目标导向设计过程的细节,更新了现行实践,重点增加 移动和触屏平台交互设计,其实《About Face 4: 交互设计精髓》多数内容适用于多种平台。 《About F......一起来看看 《About Face 4: 交互设计精髓》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

html转js在线工具
html转js在线工具

html转js在线工具