├── mvvm
├── watcher.js
├── mvvm.js
├── index.html
├── observer.js
└── compile.js
└── README.md
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### MVVM 框架
2 |
3 | 近年来前端一个明显的开发趋势就是架构从传统的 MVC 模式向 MVVM 模式迁移。在传统的 MVC 下,当前前端和后端发生数据交互后会刷新整个页面,从而导致比较差的用户体验。因此我们通过 Ajax 的方式和网关 REST API 作通讯,异步的刷新页面的某个区块,来优化和提升体验。
4 |
5 | #### MVVM 框架基本概念
6 |
7 | 
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 | 
18 |
19 | 模拟 Vue 的双向绑定流,实现了一个简单的 [MVVM 框架](https://github.com/MuYunyun/mvvm),从上图中可以看出虚线方形中就是之前提到的 ViewModel 中间介层,它充当着观察者的角色。另外可以发现双向绑定流中的 View 到 Model 其实是通过 input 的事件监听函数实现的,如果换成 React(单向绑定流) 的话,它在这一步交给状态管理工具(比如 Redux)来实现。另外双向绑定流中的 Model 到 View 其实各个 MVVM 框架实现的都是大同小异的,都用到的核心方法是 `Object.defineProperty()`,通过这个方法可以进行数据劫持,当数据发生变化时可以捕捉到相应变化,从而进行后续的处理。
20 |
21 | 
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 | 
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 |
--------------------------------------------------------------------------------