实现一个简易的 vue2

  • MVue.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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//MVue.js
const compileUtil = {
getVal(expr, vm) {
//校验参数是不是对象
if (expr.indexOf('{') === -1) {
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal]
}, vm.$data)
} else return eval(`(${expr})`)
},
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
if (typeof data[currentVal] !== 'object') {
data[currentVal] = inputVal
}
return data[currentVal]
}, vm.$data)
},
//解析双括号
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
},
text(node, expr, vm) {
let value
//处理一下元素文本是否带有{{}}标签
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], newVal => {
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getVal(args[1], vm)
})
} else value = this.getVal(expr, vm)
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, newVal => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
//TODO v-for
for(node, expr, vm) {},
model(node, expr, vm) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, newVal => {
this.updater.modelUpdater(node, newVal)
})
node.addEventListener('input', e => {
this.setVal(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
//获取methods里的函数并绑定
const fn = vm.$options.methods?.[expr]
node.addEventListener(eventName, fn.bind(vm), false)
},
bind(node, expr, vm, attrName) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, newVal => {
this.updater.bindUpdater(node, newVal, attrName)
})
this.updater.bindUpdater(node, value, attrName)
},
//更新函数
updater: {
//解析指令v-text更新
textUpdater: (node, value) => (node.textContent = value),
//解析指令v-html更新
htmlUpdater: (node, value) => (node.innerHTML = value),
//解析指令v-model更新
modelUpdater: (node, value) => (node.value = value),
bindUpdater: (node, value, attrName) => {
//处理一下参数
if (value instanceof Object) {
let objStr = ''
Object.keys(value).forEach(key => {
objStr += `${key}:${value[key]};`
node.setAttribute(attrName, objStr)
})
} else node.setAttribute(attrName, value)
}
}
}
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
//1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el)
//2.编译模板
this.compile(fragment)
//3.追加元素到根元素
this.el.appendChild(fragment)
}
/**
*
* @param {*} fragment
*/
compile(fragment) {
//1.获取子节点
const childNodes = fragment.childNodes
;[...childNodes].forEach(child => {
// 判断 元素节点/文本节点
if (this.isElementNode(child)) {
// console.log('元素', child);
this.compileElement(child)
} else {
// console.log('文本', child);
this.compileText(child)
}
//判断当前元素是否还有子节点 有则递归调用
if (child.childNodes && child.childNodes.length) this.compile(child)
})
}
/**判断自定义指令 */
isDirective(attrName) {
return attrName.startsWith('v-')
}
isEventName(attrName) {
return attrName.startsWith('@')
}
/**编译元素节点 */
compileElement(node) {
//获取元素节点所有属性
const { attributes } = node
;[...attributes].forEach(attr => {
const { name, value } = attr
if (this.isDirective(name)) {
const [, dirctive] = name.split('-') //解析自定义指令 v-xxx->xxx
const [dirName, eventName] = dirctive.split(':') //解析v-on:click->click
//更新数据 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName)
//删除有指令的标签上的属性
node.removeAttribute('v-' + dirctive)
} else if (this.isEventName(name)) {
//解析@语法糖相关指令
const [, evenName] = name.split('@') //解析自定义指令 v-xxx->xxx
compileUtil['on'](node, value, this.vm, evenName)
//删除有指令的标签上的属性
node.removeAttribute('@' + evenName)
}
})
}
/**编译元素文本 */
compileText(node) {
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
//筛选出来带有{{}}的文本
compileUtil['text'](node, content, this.vm)
}
}
/**创建文档碎片 */
node2Fragment(el) {
const f = document.createDocumentFragment()
let firstChild
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild)
}
return f
}
isElementNode(node) {
return node.nodeType === 1
}
}
class Mvue {
constructor(options) {
const { el, data } = options
this.$el = el
this.$data = data
this.$options = options
if (this.$el) {
/**实现一个数据观察者 */
new Observer(this.$data)
/**实现一个指令解析器 */
new Compile(this.$el, this)
//代理this指针this => this.$data
this.proxyData(this.$data)
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
  • 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
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
class Observer {
constructor(data) {
this.observe(data)
}
//监听对象
observe(data) {
if (data && data instanceof Object) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
//数据劫持实现
defineReactive(obj, key, value) {
this.observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
//订阅数据变化时,收集观察者
Dep.target && dep.addSUb(Dep.target)
return value
},
set: newVal => {
this.observe(newVal)
if (newVal !== value) value = newVal
//通知变化
dep.notify()
}
})
}
}

//依赖收集
class Dep {
constructor() {
this.subs = []
}
//收集观察者
addSUb(watcher) {
this.subs.push(watcher)
}
notify() {
console.log('观察者', this.subs)
this.subs.forEach(w => w.update())
}
}

//观察者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
//存储旧值
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = compileUtil.getVal(this.expr, this.vm)
Dep.target = null
return oldVal
}
update() {
const newVal = compileUtil.getVal(this.expr, this.vm)
if (newVal !== this.oldVal) this.cb(newVal)
}
}
  • index.html入口文件
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>实现vue2</title>
<style>
.red {
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="app">
<h3>{{person.name}}--{{person.age}}</h3>
<h4>{{person.fav}}</h4>
<h2>{{msg}}</h2>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div v-text="msg"></div>
<div v-bind:style='{background:"skyblue",color:"red"}' v-bind:class="color">解析v-bind:style</div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg" />
<input type="text" v-model="person.name" />
<button @click="handleClick">按钮</button>
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
const vm = new Mvue({
el: '#app',
data: {
person: {
name: 'name',
age: 18,
fav: 'game'
},
msg: 'hi qiuSu',
htmlStr: '解析v-html',
color: 'red'
},
methods: {
handleClick(a, b) {
// console.log(a,b);
console.log('click', this)
this.person.name = 'yellow'
}
}
})
</script>
</body>
</html>