• 对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     constructor(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打桩尝试,还请不要吝啬一个小星星哟!

    constructor
  • 相关阅读:
    Java 对象容器
    Java 类与对象
    Java 数组 字符 函数
    C++ 趣图
    数据管理系统想要实现的功能?
    Incorrect integer value: '' for column 'mid' at row 11366Incorrect integer value: '' for column 'mid' at row 1,自增字段为空,添加记录时出错
    关于汉字参数在链接参数中,接收后显示为问号乱码的问题该如何解决?
    原生js实现文字无缝向上滚动效果
    在php中如何用 union all统计总条数?
    在php中修改cookie值遇到的奇怪问题
  • 原文地址:https://www.cnblogs.com/dingxingxing/p/13296409.html
Copyright © 2020-2023  润新知