对Vue中的MVVM原理解析和实现
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.
对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打桩尝试,还请不要吝啬一个小星星哟!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK