简易vue框架实现

找到了个简单代码实现了vue的少许基本功能的例子,有助于了解vue源码,加深对框架的理解。遇到问题也可以从原理方面分析。

实现目标

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
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script src="./watcher.js"></script>
<script type="text/javascript">
new Mvvm({
el: '#app',
data: {
title: 'mvvm title',
name: 'mvvm name'
},
methods: {
clickMe: function () {
this.title = 'mvvm code click'
},
},
mounted: function () {
window.setTimeout(() => {
this.title = 'timeout 1000'
}, 1000)
}
})
</script>
</body>

框架图

主函数

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

function Mvvm (options) {
// 数据和方法
this.data = options.data
this.methods = options.methods

const self = this

// 将data 代理到 this
Object.keys(this.data).forEach(key =>
self.proxyKeys(key)
)

// 观察&数据劫持
observe(this.data)

// 模板编译
new Compile(options.el, this)

// 所有事情处理好后执行 mounted 函数
options.mounted.call(this)
}

Mvvm.prototype = {
proxyKeys: function(key) {
const self = this
// 这里的 get 和 set 实现了 vm.data.name 和 vm.name 的值同步
Object.defineProperty(this, key, {
get: function () {
return self.data[key]
},
set: function (newValue) {
self.data[key] = newValue
}
})
}
}

编译函数 compile

将html中的vue指令解析编译 实现绑定

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
function Compile(el, vm) {
this.vm = vm
this.el = document.querySelector(el)
this.fragment = null
this.init()
}

Compile.prototype = {
init: function() {
if (this.el) {
// 因为遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中
this.fragment = this.nodeToFragment(this.el)
// 循环处理每个节点
this.compileElement(this.fragment)
// 再加回去
this.el.appendChild(this.fragment)
} else {
console.log('Dom元素不存在')
}
},
nodeToFragment: function(el) {
const fragment = document.createDocumentFragment()
let child = el.firstChild // △ 第一个 firstChild 是 text
while(child) {
fragment.appendChild(child)
// appendChild 后 原始值为减少
child = el.firstChild
}
return fragment
},
compileElement: function(el) {
const childNodes = el.childNodes
const self = this
Array.prototype.forEach.call(childNodes, function (node) {
const reg = /\{\{(.*)\}\}/
const text = node.textContent
// 编译解析 v- 和 on:
if (self.isElementNode(node)) {
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) {
// 编译解析 {{...}}
self.compileText(node, reg.exec(text)[1])
}
// 子循环
if (node.childNodes && node.childNodes.length) { // 循环遍历子节点
self.compileElement(node)
}
})
},

compile: function (node) {
const nodeAttrs = node.attributes
const self = this

Array.prototype.forEach.call(nodeAttrs, function (attr) {
const attrName = attr.name
const exp = attr.value
const dir = attrName.substring(2)
if (self.isDirective(attrName)) { // 如果指令包含 v-
if (self.isEventDirective(dir)) { // 如果是事件指令, 包含 on:
self.compileEvent(node, self.vm, exp, dir)
} else { // v-model 指令
self.compileModel(node, self.vm, exp)
}
}
})
},

compileText: function (node, exp) { // 将 {{abc}} 替换掉
const self = this
const initText = this.vm[exp]
this.updateText(node, initText) // 初始化
new Watcher(this.vm, exp, function(value) { // 实例化订阅者
self.updateText(node, value)
})
},

compileEvent: function (node, vm, exp, dir) {
const eventType = dir.split(':')[1]
const cb = vm.methods && vm.methods[exp]

if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false)
}
},

compileModel: function (node, vm, exp) {
let val = vm[exp]
const self = this
this.modelUpdater(node, val)
node.addEventListener('input', function (e) {
const newValue = e.target.value
self.vm[exp] = newValue // 实现 view 到 model 的绑定
})
},

updateText: function (node, value) {
node.textContent = typeof value === 'undefined' ? '' : value
},

modelUpdater: function(node, value) {
node.value = typeof value === 'undefined' ? '' : value
},

isEventDirective: function(dir) {
return dir.indexOf('on:') === 0
},

isDirective: function(attr) {
return attr.indexOf('v-') === 0
},

isElementNode: function(node) {
return node.nodeType === 1
},

isTextNode: function(node) {
return node.nodeType === 3
}
}

数据劫持 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
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 通过 observe 监听数据变化,当数据变化时候,告知 Dep,调用 update 更新数据。
*/
function Dep() {
this.subs = []
}

Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
// 对应 watcher 中的 update 方法
sub.update()
})
}
}


function observe(data) {
if (!data || typeof(data) !== 'object') {
return
}
const self = this
Object.keys(data).forEach(key =>
self.defineReactive(data, key, data[key])
)
}

function defineReactive(data, key, value) {
var dep = new Dep()
observe(value) // 遍历嵌套对象
Object.defineProperty(data, key, {
get: function() {
// 有target时 才会添加订阅 (对应watcher 中 target 设定)
if (Dep.target) { // 往订阅器添加订阅者
dep.addSub(Dep.target)
}
return value
},
set: function(newValue) {
if (value !== newValue) {
console.log('值发生变化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
value = newValue
dep.notify()
}
}
})
}

Watcher: observer 和 编译后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
// Watcher 订阅者作为 observer 和 compile 之间通信的桥梁,主要做的事情是:
// 1、在自身实例化时往订阅器(dep)里面添加自己
// 2、待 model 变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm
this.exp = exp
this.value = this.get()
}

Watcher.prototype = {
update: function() {
this.run()
},

run: function() {
const value = this.vm.data[this.exp]
const oldVal = this.value
if (value !== oldVal) {
this.value = value
this.cb.call(this.vm, value)
}
},

get: function() {
Dep.target = this // 缓存自己
// 强制执行监听器里的 get 函数 进而添加订阅
const value = this.vm.data[this.exp]
Dep.target = null // 释放自己
return value
}
}