vue用了有段时间了,参考了github的文章,写了个简化版

github文章链接:https://github.com/DMQ/mvvm
如果看不懂就看我的啊,嘿嘿嘿

前言

使用Vue也有一段时间了,vue作为一个MVVM框架,最有名的就是双向绑定
一般来说当数据变化时,视图层跟着变化,这是单向的
当视图层变化时,数据也跟着变,这就是双向的
比如在vue中通过v-model绑定的输入框,当输入框内值变化时数据也跟着变化

今天就来实现一个类似于vue的双向绑定,预期效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 视图层
<div id="app">
this is {{name}}, {{age}} years old!
<h1>
{{name}}
</h1>
</div>

// 数据层
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})

Object.defineProperty的使用

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
MDN:https://dwz.cn/umKlHoaq

定义对象descriptor时注意事项:

  1. 属性描述(configurable,enumerable):
    通过这个定义的属性默认不可被删除(delete obj.xxx),默认不可遍历(for in)
  2. 值描述(value, writable):
    value和writable,value默认为undefined,writable默认为false值不能被改变
  3. 存取描述:get,set函数
  4. 值描述符和存取描述符不能同时存在

观察者模式

Subject是一个主题(网站)。Observer相当于一个个的观察者(网民),他们可以订阅Subject,当Subject更新时通知Observer,触发Observer之前定义的回调。

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
//es6实现
class Subject {
constructor(){
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state){
this.state = state
this.notifyAllObservers()
}
notifyAllObservers(){
this.observers.forEach(observer=>{
observer.update()
})
}
attach(observer) {
this.observers.push(observer)
}
}

class Observer {
constructor(name, subject){
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update,state:${this.subject.getState()}`)
}
}

let s = new Subject()
let o = new Observer('o', s)
let o2 = new Observer('o2', s)
s.setState(1)

实现原理

通过使用Object.defineProperty(数据拦截)和观察者模式实现双向绑定
原理图:

  1. 主题是什么? 一个个key,比如name
  2. 观察者是什么? 视图里面的,需要被替换成数据的地方
  3. 观察者什么时候订阅?
    一开始执行MVVM初始化时候根据el遍历dom节点,发现时候时订阅对应主题xxxx
  4. 主题什么时候通知更新?
    当xxxx改变时,通知观察者更新内容。可以在一开始就监控data通过Object.defineProperty()实现

实现单向数据流(数据=>视图)

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

// 总入口
class MVVM {
constructor(opts){
// 初始化,绑定数据到vm对象上
this.init(opts)
// 遍历所有data值,设置get和set方法
observe(this.$data)
// 遍历dom树,查找{{}}的特殊标记
this.compile()
}
init(opts){
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
compile(){
this.traverse(this.$el)
}
traverse(node){
if(node.nodeType === 1){
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
} else if(node.nodeType === 3){
this.renderText(node)
}
}
renderText(node){
let reg = /{{(.+?)}}/g
let match
while (match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()

// 初始化时绑定data里的数据到视图对应节点
node.nodeValue = node.nodeValue.replace(raw, this.$data[key])

// 对当前的key设置监听
new Observer(this, key, function (val, oldVal) {
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
}



let currentObserver = null

// 遍历data,添加监听的observe方法
function observe(data) {
if(!data || typeof data !== 'object') return
for(var key in data){

//如果下面的get方法直接返回data[key]会引起循环调用导致栈溢出
let val = data[key]

//每一个key都有一个自己的subject
let subject = new Subject()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('run get')

// currentObserver是一个全局的对象
if(currentObserver){

// 订阅某一个subject(data中的某一个键值对的key)
currentObserver.subscribeTo(subject)
}
return val
},
set: function (newVal) {
val = newVal
console.log('run set')

// 更改了值,通知所有订阅者
subject.notify()
}
})
if(typeof val === 'object'){
observe(val)
}
}
}



// 主题Subject,data中的每一个key都是一个subject
class Subject {
constructor(){
// 一个主题会有多个订阅者,比如说在视图中会有多个地方有{{name}}
this.observers = []
}
addObserver(observer){
this.observers.push(observer)
}
notify(){
this.observers.forEach(observer=>{
observer.update()
})
}
}



// 订阅者Observer
class Observer {
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb

// currentObserver是一个全局的变量,设置他有值
// 并执行下一行触发get方法,会运行observe方法中的订阅操作
currentObserver = this
this.value = this.getValue()
currentObserver = null
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal){
this.value = value
this.cb.bind(this.vm)(value, oldVal)
}
}
subscribeTo(subject){
subject.addObserver(this)
}
getValue(){

// 这里会触发data中元素的get函数
let value = this.vm.$data[this.key]
return value
}
}
`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 调用
<div id="app">
this is {{name}}, {{age}} years old!
</div>

<script type="text/javascript">
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})
</script>

实现双向绑定

在vue中,如果要对一个input框进行双向绑定,需要设置v-model指令
这里我们进行相同的设置,对于设置了v-model的input实现双向绑定
在前面已经实现单向数据流的基础上很容易实现双向绑定
在解析dom时候检测v-model指令,对应的元素绑定监听事件,当值改变时触发设置data即可

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
// 修改上面代码的MVVM
class MVVM {

// ......
traverse(node){
if(node.nodeType === 1){
this.compileNode(node)
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
} else if(node.nodeType === 3){
this.renderText(node)
}
}

// ......
compileNode(node){
let attrs = [...node.attributes]
attrs.forEach(attr=>{
if(this.isDirective(attr.name)){
let key = attr.value
node.value = this.$data[key]
new Observer(this, key, function (newVal) {
node.value = newVal
})
node.oninput = (e)=>{
this.$data[key] = e.target.value
}
}
})
}

isDirective(attrName){
return attrName === 'v-model'
}
}
`

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 调用
<div id="app">
this is {{name}}, {{age}} years old!
<h1>
{{name}}
</h1>
<input type="text" v-model="name">
</div>

<script type="text/javascript">
var vm = new MVVM({
el: '#app',
data: {
name: 'jianqi',
age: 3
}
})
</script>

优化