基于vue的理解实现一个简单的mvvm框架

在写了很多的vue项目之后,就比较想去了解vue的实现方式,因为现在的前端框架主要是Vue/React/Angular,除了React之外都可以实现数据的双向绑定,React也可以稍微做多一点,自己实现,关键问题在是数据与页面(模板)的绑定、同步数据更新/页面更新。
类vue的mvvm框架叫做 模板(model)-视图(view)-视图模板(viewmodel),核心就是将view耦合model,并且在model更新的时候操作view中的dom变化

mvvm框架流程解析

本文章的代码参考了DMQ/mvvm
vue采用了数据劫持的方式(observer)来进行视图-模板的绑定更新,

图片来自DMQ/mvvm
流程:

  • observer:数据监听,由observer对数据模型使用Object.defineProperty对数据的get、set方法进行劫持,设置数据时通知订阅者(watcher)更新,获取数据时绑定订阅者,
  • compile:模板/指令编译器,将html文件中的指令(v-model,@clickj,{{}})等特定规则的字符串解析成mvvm中的数据,并且将其添加为一个观察者,同时执行具体的dom更新、事件绑定等
  • watcher:订阅者,模板/数据观察者,一旦观察到了数据的变动,则进行数据更新
  • dep:消息订阅器,连接compile与watcher,在数据劫持中绑定watcher

mvvm框架页面示例

以一个简单的页面示范mvvm框架的实现
实现的效果:

html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack4 demo</title>
</head>
<body>
<div class="main" id="app">
<input type="text" v-model="str">
<div style="color:red">{{str}}</div>
<!-- <input type="text" v-model="child.childData"> -->
<div style="color: blue" v-text="child.childData"></div>
<div style="color: blue" v-text="text"></div>
<!-- {{str}} -->
<input type="button" value="清除" @click="clear">
<input type="button" value="设置子对象" @click="setChild">
</div>
</body>
</html>

js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Mvvm from './mvvm'

let mv = new Mvvm({
el: "#app",
data: ()=>{
return {
str : '我是测试',
someStr: '别的内容',
child: {
childData:'我是子对象',
},
text: '我是text'

}
},
methods: {
setChild(){
console.log('子对象的事件绑定')
this.child.childData = Math.round((Math.random() * 5 * 100))/100
},
clear(){
console.log('绑定事件成功')
this.str = ''
}
}
})

以上的示例,我们要实现的就是将v-text、v-model,{{}}@click与data里对应的text、str、child.childData和methods里的方法进行绑定与更新。

mvvm框架的实现

项目基于之前的webpack4-demo实现,src目录下新建mvvm目录,并且新建以下文件:index.js,compile.js,dep.js,observer.js,watcher.js。

observer.js 数据监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 实现的关键点在defineReactive方法
class Observer {
constructor(data){
this.data = data
this.walk(this.data)
}
walk(obj){
const keys = Object.keys(obj)
// 对data的所有属性进行遍历,
keys.map(key=>{
this.defineReactive(obj,key,obj[key])
})
}
// 给obj添加defineProperty属性,可响应式的取值
defineReactive(obj, key, value){
// 子对象继续进行数据劫持
if(typeof value === 'object'){
return observer(value)
}
const dep = new Dep()
let childObj = observer(value)
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: ()=>{
if (Dep.target) {
dep.depend()
// dep.addSub(Dep.target)
}
return value
},
set: (newVal)=>{
if(value === newVal){
return
}
console.log('监听到值变化了:',value,'=>',newVal);
value = newVal
childObj = observer(newVal)
// 数据发生变化,主动通知
dep.notify()
}
})
}
}

dep.js 消息订阅器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 订阅器需要实现增加订阅、移除订阅、通知订阅者
// 在这里需要注意,要添加一个 全局变量 Dep.target 作为订阅者的临时存储
let uid = 0;
// 消息订阅器
class Dep {
constructor(){
this.id = uid++;
// 初始化订阅器
this.subs = []
}
// 增加订阅
addSub(sub){
this.subs.push(sub)
}
// 移除订阅
removeSub(sub){
let index = this.subs.indexOf(sub)
if(!index === -1){
this.subs.splice(index,1)
}
}
depend(){
// 将相关的watcher添加到subs中(将暂时的watch添加到subs)addDep 为watcher的方法
Dep.target.addDep(this);
}
// 订阅对象(watcher)更新
notify(){
this.subs.map(sub => {
sub.update()
})
}
}
// 订阅器增加全局属性 target,暂存watcher
Dep.target = null
export default Dep

watcher.js 订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import Dep from './dep'

class Watcher{
constructor(vm, directiveValue, cb){
this.vm = vm
this.directiveValue = directiveValue
this.cb =cb
this.depIds = {}
// 触发属性的getter
this.value = this.get()
}

update(){
let value = this.get()
let oldValue = this.value
if(value !== oldValue){
this.value = value
// call方法 传入当前的watcher的vm作为回调的作用域
this.cb.call(this.vm, value, oldValue)
}
}
addDep(dep){
// 如果当前的depid在已有的depids不存在,则添加订阅
if(!this.depIds.hasOwnProperty(dep.id)){
dep.addSub(this)
this.depIds[dep.id] = dep
}
}
get(){
Dep.target = this
// let value = this.vm[this.directiveValue] //触发observer的get,从而触发订阅
let value = this.getVal(this.vm, this.directiveValue)
Dep.target = null
return value
}
// child.childData的形式也能获取数据
getVal(vm, directiveValue){
// 迭代获取数据,可获取子对象
let value = vm;
directiveValue = directiveValue.split('.')
directiveValue.map(item=>{
value = value[item]
})
return value
}
}

export default Watcher

compile.js 模板/指令解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// 主要实现点在与将dom对象fragment化,解析成html片段的形式进行再次解析,再区分的指令与文本节点,再做不同的操作,添加事件监听、更新文本、更新html、添加input监听(v-model)
import Watcher from './watcher'

class Compile {
constructor(el, vm){
// vm mvvm实例
this.vm = vm
this.el = this.isElementNode(el) ? el : document.querySelector(el)
if(this.el){
this.$fragment = this.node2Fragment(this.el)
this.init()
this.el.appendChild(this.$fragment)
}
}
init(){
this.compileFragment(this.$fragment)
}
// 遍历转换完成的html片段(fragment)
compileFragment(fragment){
let childNodes = fragment.childNodes

for (const node of childNodes) {
let text = node.textContent
// {{}} 格式的文本
let reg = /\{\{(.*)\}\}/

if(this.isElementNode(node)){
this.compileNode(node)
}else if(this.isTextNode(node) && reg.test(text)){
// 正则值 RegExp.$1
let directiveValue = RegExp.$1
compileUtils.text(this.vm, node, directiveValue)
}
// 递归循环子节点
if(node.childNodes && node.childNodes.length){
this.compileFragment(node)
}
}
}
compileNode(node){
let nodeAttrs = node.attributes
for (const attr of nodeAttrs) {
let attrName = attr.name
if(this.isDirective(attrName)){
// 指令值
let directiveValue = attr.value
// 指令名称
let directiveName = ''
// 如果是动作指令 @开头的
if(this.isEventDirective(attrName)){
directiveName = attrName.substring(1)
compileUtils.eventHandler(this.vm, node, directiveValue, directiveName)
}else{
directiveName = attrName.substring(2)
compileUtils[directiveName] && compileUtils[directiveName](this.vm, node,directiveValue,directiveName)
}
node.removeAttribute(attrName)

}
}
}
// node节点转换成html片段
node2Fragment(el){
let fragment = document.createDocumentFragment()
let child
while (child = el.firstChild){
fragment.appendChild(child)
}

return fragment
}
// TODO 待使用正则匹配
// 判断是否是指令属性
isDirective(attrname){
// v-开头 或者 @开头
let reg = /^v-|^@/
return reg.test(attrname)
// return attrname.indexOf('v-') === 0 || attrname.indexOf('@') === 0
}
isEventDirective(directiveName){
let reg = /^@/
return reg.test(directiveName)
}
// 判断是否是node类型 div/p 等html节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否是文本类型 {{}}
isTextNode(node) {
return node.nodeType === 3;
}

}

const compileUtils = {
/**参数注释
* vm: vm实例
* node: html节点
* directiveValue:@click="clean" 里的clean 也就是执行的函数名称
* directiveName:@click 里的click,也就是事件名称
*/
// vm实例,node html节点,directiveValue: @click 的click,
eventHandler(vm, node, directiveValue, directiveName){
let method = vm.methods && vm.methods[directiveValue]
if(method && directiveName){
// true 捕获(由上倒下) false 冒泡(由下到上)
node.addEventListener(directiveName, method.bind(vm), false)
}
},
/**参数注释
* vm: vm实例
* node: html节点
* directiveValue:{{str}} 里的str
* directiveName:@click 里的click,也就是事件名称
*/
text(vm, node, directiveValue, directiveName){
// 第一次初始化
this.updateText(node,this.getVal(vm,directiveValue))
// 实例化订阅并添加watcher
new Watcher(vm, directiveValue, (value, oldValue)=>{
this.updateText(node,value)
})
},
html(vm, node, directiveValue){
this.updateHtml(node,this.getVal(vm,directiveValue))
new Watcher(vm, directiveValue, (value, oldValue)=>{
this.updateHtml(node,value)
})
},
model(vm, node, directiveValue){
let modelValue = this.getVal(vm,directiveValue)
this.updateModel(node, modelValue)
new Watcher(vm, directiveValue, (value, oldValue)=>{
this.updateModel(node, value)
})
node.addEventListener('input', (e)=>{
let newValue = e.target.value
if(newValue === modelValue){
return
}
// vm[directiveValue] = newValue
this.setVal(vm, directiveValue,newValue)
modelValue = newValue
})
},
updateText(node, value){
node.textContent = typeof value === 'undefined' ? '' : value
},
updateHtml(node, value){
node.innerHTML = typeof value === 'undefined' ? '' : value
},
updateModel(node, value){
node.value = typeof value === 'undefined' ? '' : value
},
getVal(vm, directiveValue){
// 迭代获取数据,可获取子对象数据
let value = vm;
directiveValue = directiveValue.split('.')
directiveValue.map(item=>{
value = value[item]
})
return value
},
setVal(vm, directiveValue,newValue){
// 迭代设置数据,可设置子对象
let value = vm
directiveValue = directiveValue.split('.')
directiveValue.map((item,index)=>{
// 一直遍历到 directiveValue 的最后一个,比如[child,data]
// 如果不是最后一个,那么就把新值赋给value,继续循环
// 如果 directiveValue 不是子对象那么,就直接赋值
if(index < directiveValue.length -1){
value = value[item]
}else{
value[item] = newValue
}
})
},
}

export default Compile

以上完成了4个单独组件的编写,下面还要对上面的几个模块进行整合,在index.js里实现他
index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 在这里生成 observer类、compile类,还要通过Object.defineProperty方法实现一个数据代理功能
import observer from './observer'
import Compile from './compile';
class Mvvm {
constructor(options){
this.options = options
this.el = options.el
this.data = options.data()
this.methods = options.methods
// 数据代理
Object.keys(this.data).map(item=>{
this.proxyData(item)
})
let ob = observer(this.data)
this.compile = new Compile(this.el,this)
// console.log(ob.data.text)
}
// 数据代理,this.str = this.data.str,也采用数据劫持的方式
proxyData(key){
Object.defineProperty(this,key,{
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: ()=>{
return this.data[key]
},
set: (newValue)=>{
this.data[key] = newValue
}
})
}
}

export default Mvvm

这样就完成了非常简化版的mvvm框架,使用他见以上的示例代码
完整的代码见mvvm