├── README.md └── mvvm ├── compile.js ├── index.html ├── mvvm.js ├── observer.js └── watcher.js /README.md: -------------------------------------------------------------------------------- 1 | ### MVVM 框架 2 | 3 | 近年来前端一个明显的开发趋势就是架构从传统的 MVC 模式向 MVVM 模式迁移。在传统的 MVC 下,当前前端和后端发生数据交互后会刷新整个页面,从而导致比较差的用户体验。因此我们通过 Ajax 的方式和网关 REST API 作通讯,异步的刷新页面的某个区块,来优化和提升体验。 4 | 5 | #### MVVM 框架基本概念 6 | 7 | ![](http://oqhtscus0.bkt.clouddn.com/203d14fb02edc5f37ae3841a2372434b.jpg-400) 8 | 9 | 在 MVVM 框架中,View(视图) 和 Model(数据) 是不可以直接通讯的,在它们之间存在着 ViewModel 这个中间介充当着观察者的角色。当用户操作 View(视图),ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model(数据) 发生改变,ViewModel 也能感知到变化,使 View 作出相应更新。这个一来一回的过程就是我们所熟知的双向绑定。 10 | 11 | #### MVVM 框架的应用场景 12 | 13 | MVVM 框架的好处显而易见:当前端对数据进行操作的时候,可以通过 Ajax 请求对数据持久化,只需改变 dom 里需要改变的那部分数据内容,而不必刷新整个页面。特别是在移动端,刷新页面的代价太昂贵。虽然有些资源会被缓存,但是页面的 dom、css、js 都会被浏览器重新解析一遍,因此移动端页面通常会被做成 SPA 单页应用。由此在这基础上诞生了很多 MVVM 框架,比如 React.js、Vue.js、Angular.js 等等。 14 | 15 | ### MVVM 框架的简单实现 16 | 17 | ![](http://oqhtscus0.bkt.clouddn.com/ecac404dd0a757b06ae1bd1b5c8212ef.jpg-600) 18 | 19 | 模拟 Vue 的双向绑定流,实现了一个简单的 [MVVM 框架](https://github.com/MuYunyun/mvvm),从上图中可以看出虚线方形中就是之前提到的 ViewModel 中间介层,它充当着观察者的角色。另外可以发现双向绑定流中的 View 到 Model 其实是通过 input 的事件监听函数实现的,如果换成 React(单向绑定流) 的话,它在这一步交给状态管理工具(比如 Redux)来实现。另外双向绑定流中的 Model 到 View 其实各个 MVVM 框架实现的都是大同小异的,都用到的核心方法是 `Object.defineProperty()`,通过这个方法可以进行数据劫持,当数据发生变化时可以捕捉到相应变化,从而进行后续的处理。 20 | 21 | ![](http://oqhtscus0.bkt.clouddn.com/4b8db3d45cd6f37935e9bec42f0095c7.jpg-300) 22 | 23 | #### Mvvm(入口文件) 的实现 24 | 25 | 一般会这样调用 Mvvm 框架 26 | 27 | ```js 28 | const vm = new Mvvm({ 29 | el: '#app', 30 | data: { 31 | title: 'mvvm title', 32 | name: 'mvvm name' 33 | }, 34 | }) 35 | ``` 36 | 37 | 但是这样子的话,如果要得到 title 属性就要形如 vm.data.title 这样取得,为了让 vm.title 就能获得 title 属性,从而在 Mvvm 的 prototype 上加上一个代理方法,代码如下: 38 | ```js 39 | function Mvvm (options) { 40 | this.data = options.data 41 | 42 | const self = this 43 | Object.keys(this.data).forEach(key => 44 | self.proxyKeys(key) 45 | ) 46 | } 47 | 48 | Mvvm.prototype = { 49 | proxyKeys: function(key) { 50 | const self = this 51 | Object.defineProperty(this, key, { 52 | get: function () { // 这里的 get 和 set 实现了 vm.data.title 和 vm.title 的值同步 53 | return self.data[key] 54 | }, 55 | set: function (newValue) { 56 | self.data[key] = newValue 57 | } 58 | }) 59 | } 60 | } 61 | ``` 62 | 63 | 实现了代理方法后,就步入主流程的实现 64 | 65 | ```js 66 | function Mvvm (options) { 67 | this.data = options.data 68 | // ... 69 | observe(this.data) 70 | new Compile(options.el, this) 71 | } 72 | ``` 73 | 74 | #### observer(观察者) 的实现 75 | 76 | observer 的职责是监听 Model(JS 对象) 的变化,最核心的部分就是用到了 Object.defineProperty() 的 get 和 set 方法,当要获取 Model(JS 对象) 的值时,会自动调用 get 方法;当改动了 Model(JS 对象) 的值时,会自动调用 set 方法;从而实现了对数据的劫持,代码如下所示。 77 | 78 | ```js 79 | let data = { 80 | number: 0 81 | } 82 | 83 | observe(data) 84 | 85 | data.number = 1 // 值发生变化 86 | 87 | function observe(data) { 88 | if (!data || typeof(data) !== 'object') { 89 | return 90 | } 91 | const self = this 92 | Object.keys(data).forEach(key => 93 | self.defineReactive(data, key, data[key]) 94 | ) 95 | } 96 | 97 | function defineReactive(data, key, value) { 98 | observe(value) // 遍历嵌套对象 99 | Object.defineProperty(data, key, { 100 | get: function() { 101 | return value 102 | }, 103 | set: function(newValue) { 104 | if (value !== newValue) { 105 | console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value) 106 | value = newValue 107 | } 108 | } 109 | }) 110 | } 111 | ``` 112 | 113 | 运行代码,可以看到控制台输出 `值发生变化 newValue:1 oldValue:0`,至此就完成了 observer 的逻辑。 114 | 115 | #### Dep(订阅者数组) 和 watcher(订阅者) 的关系 116 | 117 | 观测到变化后,我们总要通知给特定的人群,让他们做出相应的处理吧。为了更方便地理解,我们可以把订阅当成是订阅了一个微信公众号,当微信公众号的内容有更新时,那么它会把内容推送(update) 到订阅了它的人。 118 | 119 | ![](http://oqhtscus0.bkt.clouddn.com/42bd217acd8b2ef5c76de1ca65ba7581.jpg-200) 120 | 121 | 那么订阅了同个微信公众号的人有成千上万个,那么首先想到的就是要 new Array() 去存放这些人(html 节点)吧。于是就有了如下代码: 122 | 123 | ```js 124 | // observer.js 125 | function Dep() { 126 | this.subs = [] // 存放订阅者 127 | } 128 | 129 | Dep.prototype = { 130 | addSub: function(sub) { // 添加订阅者 131 | this.subs.push(sub) 132 | }, 133 | notify: function() { // 通知订阅者更新 134 | this.subs.forEach(function(sub) { 135 | sub.update() 136 | }) 137 | } 138 | } 139 | 140 | function observe(data) {...} 141 | 142 | function defineReactive(data, key, value) { 143 | var dep = new Dep() 144 | observe(value) // 遍历嵌套对象 145 | Object.defineProperty(data, key, { 146 | get: function() { 147 | if (Dep.target) { // 往订阅器添加订阅者 148 | dep.addSub(Dep.target) 149 | } 150 | return value 151 | }, 152 | set: function(newValue) { 153 | if (value !== newValue) { 154 | console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value) 155 | value = newValue 156 | dep.notify() 157 | } 158 | } 159 | }) 160 | } 161 | ``` 162 | 163 | 初看代码也比较顺畅了,但可能会卡在 `Dep.target` 和 `sub.update`,由此自然而然地将目光移向 watcher, 164 | 165 | ```js 166 | // watcher.js 167 | function Watcher(vm, exp, cb) { 168 | this.vm = vm 169 | this.exp = exp 170 | this.cb = cb 171 | this.value = this.get() 172 | } 173 | 174 | Watcher.prototype = { 175 | update: function() { 176 | this.run() 177 | }, 178 | 179 | run: function() { 180 | // ... 181 | if (value !== oldVal) { 182 | this.cb.call(this.vm, value) // 触发 compile 中的回调 183 | } 184 | }, 185 | 186 | get: function() { 187 | Dep.target = this // 缓存自己 188 | const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数 189 | Dep.target = null // 释放自己 190 | return value 191 | } 192 | } 193 | ``` 194 | 195 | 从代码中可以看到当构造 Watcher 实例时,会调用 get() 方法,接着重点关注 `const value = this.vm.data[this.exp]` 这句,前面说了当要获取 Model(JS 对象) 的值时,会自动调用 Object.defineProperty 的 get 方法,也就是当执行完这句的时候,Dep.target 的值传进了 observer.js 中的 Object.defineProperty 的 get 方法中。同时也一目了然地在 Watcher.prototype 中发现了 update 方法,其作用即触发 compile 中绑定的回调来更新界面。至此解释了 Observer 中 Dep.target 和 sub.update 的由来。 196 | 197 | 来归纳下 Watcher 的作用,其充当了 observer 和 compile 的桥梁。 198 | 199 | 1 在自身实例化的过程中,往订阅器(dep) 中添加自己 200 | 201 | 2 当 model 发生变动,dep.notify() 通知时,其能调用自身的 update 函数,并触发 compile 绑定的回调函数实现视图更新 202 | 203 | 最后再来看下生成 Watcher 实例的 compile.js 文件。 204 | 205 | #### compile(编译) 的实现 206 | 207 | 首先遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成 fragment(文档碎片) 进行解析编译,解析完成,再将 fragment 添加回原来的真实 dom 节点中。代码如下: 208 | 209 | ```js 210 | function Compile(el, vm) { 211 | this.vm = vm 212 | this.el = document.querySelector(el) 213 | this.fragment = null 214 | this.init() 215 | } 216 | 217 | Compile.prototype = { 218 | init: function() { 219 | if (this.el) { 220 | this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片 221 | this.compileElement(this.fragment) // 对 fragment 进行编译解析 222 | this.el.appendChild(this.fragment) 223 | } 224 | }, 225 | nodeToFragment: function(el) { 226 | const fragment = document.createDocumentFragment() 227 | let child = el.firstChild // △ 第一个 firstChild 是 text 228 | while(child) { 229 | fragment.appendChild(child) 230 | child = el.firstChild 231 | } 232 | return fragment 233 | }, 234 | compileElement: function(el) {...}, 235 | } 236 | ``` 237 | 238 | 这个简单的 mvvm 框架在对 fragment 编译解析的过程中对 `{{}} 文本元素`、`v-on:click 事件指令`、`v-model 指令`三种类型进行了相应的处理。 239 | 240 | ```js 241 | Compile.prototype = { 242 | init: function() { 243 | if (this.el) { 244 | this.fragment = this.nodeToFragment(this.el) // 将节点转为 fragment 文档碎片 245 | this.compileElement(this.fragment) // 对 fragment 进行编译解析 246 | this.el.appendChild(this.fragment) 247 | } 248 | }, 249 | nodeToFragment: function(el) {...}, 250 | compileElement: function(el) {...}, 251 | compileText: function (node, exp) { // 对文本类型进行处理,将 {{abc}} 替换掉 252 | const self = this 253 | const initText = this.vm[exp] 254 | this.updateText(node, initText) // 初始化 255 | new Watcher(this.vm, exp, function(value) { // 实例化订阅者 256 | self.updateText(node, value) 257 | }) 258 | }, 259 | 260 | compileEvent: function (node, vm, exp, dir) { // 对事件指令进行处理 261 | const eventType = dir.split(':')[1] 262 | const cb = vm.methods && vm.methods[exp] 263 | 264 | if (eventType && cb) { 265 | node.addEventListener(eventType, cb.bind(vm), false) 266 | } 267 | }, 268 | 269 | compileModel: function (node, vm, exp) { // 对 v-model 进行处理 270 | let val = vm[exp] 271 | const self = this 272 | this.modelUpdater(node, val) 273 | node.addEventListener('input', function (e) { 274 | const newValue = e.target.value 275 | self.vm[exp] = newValue // 实现 view 到 model 的绑定 276 | }) 277 | }, 278 | } 279 | ``` 280 | 281 | 在上述代码的 compileTest 函数中看到了期盼已久的 Watcher 实例化,对 Watcher 作用模糊的朋友可以往上回顾下 Watcher 的作用。另外在 compileModel 函数中看到了本文最开始提到的双向绑定流中的 View 到 Model 是借助 input 监听事件变化实现的。 282 | 283 | ### 项目地址 284 | 285 | 本文记录了些阅读 mvvm 框架源码关于双向绑定的心得,并动手实践了一个简版的 mvvm 框架,不足之处在所难免,欢迎指正。 286 | 287 | [项目演示](http://muyunyun.cn/mvvm/) 288 | 289 | [项目地址](https://github.com/MuYunyun/mvvm) 290 | 291 | 292 | -------------------------------------------------------------------------------- /mvvm/compile.js: -------------------------------------------------------------------------------- 1 | function Compile(el, vm) { 2 | this.vm = vm 3 | this.el = document.querySelector(el) 4 | this.fragment = null 5 | this.init() 6 | } 7 | 8 | Compile.prototype = { 9 | init: function() { 10 | if (this.el) { 11 | this.fragment = this.nodeToFragment(this.el) // 因为遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中 12 | this.compileElement(this.fragment) 13 | this.el.appendChild(this.fragment) 14 | } else { 15 | console.log('Dom元素不存在') 16 | } 17 | }, 18 | nodeToFragment: function(el) { 19 | const fragment = document.createDocumentFragment() 20 | let child = el.firstChild // △ 第一个 firstChild 是 text 21 | while(child) { 22 | fragment.appendChild(child) 23 | child = el.firstChild 24 | } 25 | return fragment 26 | }, 27 | compileElement: function(el) { 28 | const childNodes = el.childNodes 29 | const self = this 30 | Array.prototype.forEach.call(childNodes, function (node) { 31 | const reg = /\{\{(.*)\}\}/ 32 | const text = node.textContent 33 | if (self.isElementNode(node)) { 34 | self.compile(node) 35 | } else if (self.isTextNode(node) && reg.test(text)) { 36 | self.compileText(node, reg.exec(text)[1]) 37 | } 38 | 39 | if (node.childNodes && node.childNodes.length) { // 循环遍历子节点 40 | self.compileElement(node) 41 | } 42 | }) 43 | }, 44 | 45 | compile: function (node) { 46 | const nodeAttrs = node.attributes 47 | const self = this 48 | 49 | Array.prototype.forEach.call(nodeAttrs, function (attr) { 50 | const attrName = attr.name 51 | const exp = attr.value 52 | const dir = attrName.substring(2) 53 | if (self.isDirective(attrName)) { // 如果指令包含 v- 54 | if (self.isEventDirective(dir)) { // 如果是事件指令, 包含 on: 55 | self.compileEvent(node, self.vm, exp, dir) 56 | } else { // v-model 指令 57 | self.compileModel(node, self.vm, exp) 58 | } 59 | } 60 | }) 61 | }, 62 | 63 | compileText: function (node, exp) { // 将 {{abc}} 替换掉 64 | const self = this 65 | const initText = this.vm[exp] 66 | this.updateText(node, initText) // 初始化 67 | new Watcher(this.vm, exp, function(value) { // 实例化订阅者 68 | self.updateText(node, value) 69 | }) 70 | }, 71 | 72 | compileEvent: function (node, vm, exp, dir) { 73 | const eventType = dir.split(':')[1] 74 | const cb = vm.methods && vm.methods[exp] 75 | 76 | if (eventType && cb) { 77 | node.addEventListener(eventType, cb.bind(vm), false) 78 | } 79 | }, 80 | 81 | compileModel: function (node, vm, exp) { 82 | let val = vm[exp] 83 | const self = this 84 | this.modelUpdater(node, val) 85 | node.addEventListener('input', function (e) { 86 | const newValue = e.target.value 87 | self.vm[exp] = newValue // 实现 view 到 model 的绑定 88 | }) 89 | }, 90 | 91 | updateText: function (node, value) { 92 | node.textContent = typeof value === 'undefined' ? '' : value 93 | }, 94 | 95 | modelUpdater: function(node, value) { 96 | node.value = typeof value === 'undefined' ? '' : value 97 | }, 98 | 99 | isEventDirective: function(dir) { 100 | return dir.indexOf('on:') === 0 101 | }, 102 | 103 | isDirective: function(attr) { 104 | return attr.indexOf('v-') === 0 105 | }, 106 | 107 | isElementNode: function(node) { 108 | return node.nodeType === 1 109 | }, 110 | 111 | isTextNode: function(node) { 112 | return node.nodeType === 3 113 | } 114 | } -------------------------------------------------------------------------------- /mvvm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |

{{title}}

12 | 13 |

{{name}}

14 | 15 |
16 | 17 | 18 | 19 | 20 | 39 | 40 | -------------------------------------------------------------------------------- /mvvm/mvvm.js: -------------------------------------------------------------------------------- 1 | // 代理方法的目的是为了在实例对象上直接修改或获取相应属性 2 | // var vm = new MVVM({data: {name: 'Mvvm'}}); vm.data.name 3 | // => var vm = new MVVM({data: {name: 'Mvvm'}}); vm.name 4 | function Mvvm (options) { 5 | this.data = options.data 6 | this.methods = options.methods 7 | 8 | const self = this 9 | Object.keys(this.data).forEach(key => 10 | self.proxyKeys(key) 11 | ) 12 | observe(this.data) 13 | new Compile(options.el, this) 14 | options.mounted.call(this) // 所有事情处理好后执行 mounted 函数 15 | } 16 | 17 | Mvvm.prototype = { 18 | proxyKeys: function(key) { 19 | const self = this 20 | Object.defineProperty(this, key, { 21 | get: function () { // 这里的 get 和 set 实现了 vm.data.name 和 vm.name 的值同步 22 | return self.data[key] 23 | }, 24 | set: function (newValue) { 25 | self.data[key] = newValue 26 | } 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /mvvm/observer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通过 observe 监听数据变化,当数据变化时候,告知 Dep,调用 update 更新数据。 3 | */ 4 | function Dep() { 5 | this.subs = [] 6 | } 7 | 8 | Dep.prototype = { 9 | addSub: function(sub) { 10 | this.subs.push(sub) 11 | }, 12 | notify: function() { 13 | this.subs.forEach(function(sub) { 14 | sub.update() 15 | }) 16 | } 17 | } 18 | 19 | // let data = { 20 | // number: 0 21 | // } 22 | 23 | // observe(data) 24 | 25 | // data.number = 1 // 值发生变化 26 | 27 | function observe(data) { 28 | if (!data || typeof(data) !== 'object') { 29 | return 30 | } 31 | const self = this 32 | Object.keys(data).forEach(key => 33 | self.defineReactive(data, key, data[key]) 34 | ) 35 | } 36 | 37 | function defineReactive(data, key, value) { 38 | var dep = new Dep() 39 | observe(value) // 遍历嵌套对象 40 | Object.defineProperty(data, key, { 41 | get: function() { 42 | if (Dep.target) { // 往订阅器添加订阅者 43 | dep.addSub(Dep.target) 44 | } 45 | return value 46 | }, 47 | set: function(newValue) { 48 | if (value !== newValue) { 49 | console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value) 50 | value = newValue 51 | dep.notify() 52 | } 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /mvvm/watcher.js: -------------------------------------------------------------------------------- 1 | // Watcher 订阅者作为 observer 和 compile 之间通信的桥梁,主要做的事情是: 2 | // 1、在自身实例化时往订阅器(dep)里面添加自己 3 | // 2、待 model 变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调 4 | function Watcher(vm, exp, cb) { 5 | this.cb = cb 6 | this.vm = vm 7 | this.exp = exp 8 | this.value = this.get() 9 | } 10 | 11 | Watcher.prototype = { 12 | update: function() { 13 | this.run() 14 | }, 15 | 16 | run: function() { 17 | const value = this.vm.data[this.exp] 18 | const oldVal = this.value 19 | if (value !== oldVal) { 20 | this.value = value 21 | this.cb.call(this.vm, value) 22 | } 23 | }, 24 | 25 | get: function() { 26 | Dep.target = this // 缓存自己 27 | const value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数 28 | Dep.target = null // 释放自己 29 | return value 30 | } 31 | } --------------------------------------------------------------------------------