下面由我阿巴阿巴的详细走一遍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、观察者是什么时候进行绑定的?又是如何工作实现了数据驱动视图,视图驱动数据驱动视图的。