miniVue v1
小米糖糖 11/13/2020 vuemini
# Vue 原理
- 利用 Object.defineProperty/Proxy 对 js 对象进行拦截
- 在取值操作中进行依赖收集
- 在赋值操作中进行更新通知
- 通过 Vdom 进行高性能 diff 变化,达到最小更新 dom 提升性能
# 过程
- 响应式解析
- 编译 把非 render 转换成 render 函数,比如 template dom 节点等
- 渲染 进行 render 计算 Vdom
- 挂载 把 Vdom 转换成真实节点并进行挂载
- 更新 监听数据变化,diff 计算,更新页面
# mini 版本实现
# mini 代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>miniVue</title>
<style>
.num {
width: 100px;
margin: 10px;
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
}
.red {
color: red;
}
.green {
color: green;
}
</style>
</head>
<body>
<div id="app">
<h2 a='1' :class='titleClass'>{{title}}</h2>
<div class='green'>{{desc}}</div>
<div class="num">{{num}}</div>
<button @click='addNum'>add</button>
<button v-on:click='delNum'>minus</button>
<hr>
<input type="text" v-model='val'>
<div v-text='val'></div>
</div>
</body>
<script src='./utils.js'></script>
<script src='./watch.js'></script>
<script src='./observe.js'></script>
<script src='./vue.js'></script>
<script>
const app = new Vue({
el: '#app',
data: {
title: "这是一个demo",
desc: '一个mini版本的vue简单实现',
num: 1,
val: "双向绑定",
titleClass: "red"
},
methods: {
addNum() {
console.log('add')
this.num++
},
delNum() {
this.num--
}
}
})
</script>
</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
60
61
62
63
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
utils.js
function isString(str) {
return typeof str === 'string'
}
function isObj(obj) {
return typeof obj === 'object' && obj !== null
}
function parseEl(el) {
if (isString(el)) {
if (el.startsWith('#')) {
return document.getElementById(el.substr(1))
}
return document.querySelector(el)
}
return el
}
function isMulti(str) {
return /\{\{(.*)\}\}/.test(str)
}
function isDir(name) {
if (name.startsWith('v-')) return name.slice(2).split(':')
if (name.startsWith('@')) return ['on', name.slice(1)]
if (name.startsWith(':')) return ['bind', name.slice(1)]
}
function isVModel(name) {
return name === 'v-model'
}
function addEvent(node, eventName, handle) {
node.addEventListener(eventName, handle, false)
}
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
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
watch.js
// Dep 就是依赖收集的媒介,存储着一个key所对应的所有依赖项
class Dep {
constructor() {
this.deps = []
}
add(dep) {
this.deps.push(dep)
}
notice(val) {
this.deps.forEach(d => d.update(val))
}
}
// 监听对象,每一处依赖对应一个watcher
class Watch {
constructor(vm, key, fn) {
this.$vm = vm
// 生成一个依赖,这样在get的时候就会进行收集
Dep.target = this
// 这一步是重点,在此处就自动获取一次,触发 get 从而进行收集
vm[key]
Dep.target = null
this.fn = fn
}
update(val) {
this.fn(val)
}
}
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
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
observe.js
// 给定一个对象 key value 建立 get set监听函数,
// 在get里面设置依赖收集
// 在set 中进行赋值,并触发相应的依赖项进行跟新
function observe(obj, key, val) {
if (!isObj(obj)) return
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 这一步比较关键,当存在依赖,需要收集时 进行必要的收集
if (Dep.target) { dep.add(Dep.target) }
return val
},
set(newVal) {
val = newVal
// 触发依赖通知
dep.notice(newVal)
}
})
}
// 把一个对象的属性代理到另一个对象
// 从而实现 便捷访问 比如 可以通过 this.a 直接访问到 this.data.a
function proxy(obj, sourceKey) {
if (!isObj(obj)) return
let source = obj[sourceKey]
if (!isObj(source)) return
Object.keys(source).forEach(key => {
Object.defineProperty(obj, key, {
get() {
return source[key]
},
set(val) {
source[key] = val
}
})
})
}
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
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
vue.js
class Vue {
constructor(options) {
this.$options = options
const { el, data, methods } = options
// 解析 挂载点
this.$el = parseEl(el)
this.$data = data
this.$methods = methods
// 建立 双向绑定
this.ob(data)
// 代理 data methods 到 this上
proxy(this, '$data')
proxy(this, '$methods')
// 尝试挂载
this.mount()
}
ob(obj) {
// 此处 值做了对象的处理, 完善的应该增加对数组的处理,以及递归处理
if (!isObj(obj)) return
Object.keys(obj).forEach(key => {
observe(obj, key, obj[key])
})
}
mount(el) {
// 挂载并进行编译 渲染
el = el || this.$el
if (el) this.compile(el)
}
compile(node) {
if (!node) return
// 获取所有的子节点,进行相对应的解析,并递归
let childNodes = [...node.childNodes]
if (childNodes && childNodes.length) {
childNodes.forEach(childNode => {
let nodeType = childNode.nodeType
switch (nodeType) {
case 1:// 元素
this.compileEl(childNode)
this.compile(childNode)
break;
case 3: // 文本
this.compileText(childNode)
break;
}
})
}
}
// 解析元素节点,主要是对属性进行设置,以及添加事件等
compileEl(node) {
let attrs = [...node.attributes]
attrs.forEach(at => {
const { name, value } = at
// 判断是否是指令,如果是指令形式,并且已经存在对应的解析器,则调用解析器进行解析
const dirParsed = isDir(name) // v-on v-model v-text v-html
if (dirParsed) {
const [dir, exp] = dirParsed
this[dir] && this[dir](node, exp, value)
return
}
node[name] = this[value]
})
}
// 事件解析器
on(node, exp, name) {
addEvent(node, exp.toLocaleLowerCase(), e => this[name](e, this))
}
// v-text解析器
text(node, exp, val) {
this.update(node, 'InnerText', val)
}
// bind : 解析器
bind(node, exp, val) {
if (exp === 'class') this.update(node, 'Class', val)
}
// v-model解析器 Vue中会根据元素类型 做不同的解析,此处比较简单。我们认为就是input
model(node, exp, value) {
addEvent(node, 'input', e => {
const val = e.target.value
this[value] = val
})
const updater = (e) => node.value = e
new Watch(this, value, updater)
updater(this[value])
}
// 对文本节点进行处理 如果是 {{attr}}形式,则需要进行处理,替换成data中的数据
compileText(node) {
if (isMulti(node.nodeValue)) {
this.update(node, 'Text', RegExp.$1)
}
}
// 建立更新 并进行监听
update(node, dir, val) {
let updateFun = this['update' + dir]
if (updateFun) {
let updater = (v) => updateFun(node, v)
new Watch(this, val, updater)
updater(this[val])
}
}
// 更新器 有text html 等等
updateText(node, val) {
node.nodeValue = val
}
updateClass(node, val) {
node.className = val
}
updateInnerText(node, val) {
node.innerText = val
}
}
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
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