手写一个Vue.js

目标

实现一个简约版的 Vue.js,包含以下核心功能:

  1. 响应式系统:通过数据劫持实现数据的响应式更新。
  2. 模板解析:解析 {{}} 语法,将数据绑定到视图。
  3. 双向数据绑定:通过 v-model 实现表单元素与数据的双向绑定。

实现方案表

1. 项目初始化

  • 创建一个空项目,初始化 package.json
  • 创建入口文件 index.html 和主逻辑文件 mini-vue.js

2. 响应式系统

  • 实现数据劫持,监听数据的变化。
  • 使用 Object.definePropertyProxy 实现数据的 gettersetter
  • 实现依赖收集和派发更新机制。

3. 模板解析

  • 实现 {{}} 语法的解析,将数据绑定到 DOM。
  • 使用正则表达式匹配 {{}} 中的表达式,并替换为实际数据。

4. 双向数据绑定

  • 实现 v-model 指令,将表单元素的值与数据进行绑定。
  • 监听表单元素的 input 事件,更新数据;同时监听数据变化,更新表单元素的值。

5. 整合与测试

  • 将响应式系统、模板解析和双向数据绑定整合到一起。
  • 编写测试用例,验证功能是否正常工作。

一步步实现

1. 项目初始化

1.1 创建项目

1
2
3
mkdir mini-vue
cd mini-vue
npm init -y

1.2 创建文件

  • index.html:用于测试 Vue 功能。
  • mini-vue.js:实现 Vue 核心逻辑。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Vue</title>
</head>
<body>
<div id="app">
<p>{{ message }}</p>
<input v-model="message" />
</div>
<script src="./mini-vue.js"></script>
<script>
const app = new MiniVue({
el: '#app',
data: {
message: 'Hello, Mini Vue!'
}
});
</script>
</body>
</html>

2. 响应式系统

2.1 实现数据劫持

mini-vue.js 中,创建一个 MiniVue 类,并实现数据的响应式。

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
class MiniVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
}

// 数据劫持
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}

// 定义响应式
defineReactive(obj, key, val) {
this.observe(val); // 递归处理嵌套对象
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
console.log(`属性 ${key} 更新为 ${newVal}`);
}
});
}
}

2.2 测试响应式

index.html 中测试数据劫持功能:

1
2
3
4
5
6
7
8
9
const app = new MiniVue({
el: '#app',
data: {
message: 'Hello, Mini Vue!'
}
});

// 测试数据劫持
app.$data.message = 'Hello, World!'; // 控制台输出:属性 message 更新为 Hello, World!

3. 模板解析

3.1 实现 双括号 解析

MiniVue 类中添加模板解析功能。

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
class MiniVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
this.compile(document.querySelector(options.el));
}

// 模板解析
compile(el) {
const nodes = el.childNodes;
nodes.forEach(node => {
if (node.nodeType === 1) { // 元素节点
this.compileElement(node);
} else if (node.nodeType === 3) { // 文本节点
this.compileText(node);
}
});
}

// 解析元素节点
compileElement(node) {
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-model') {
const key = attr.value;
node.value = this.$data[key];
node.addEventListener('input', () => {
this.$data[key] = node.value;
});
}
});
}

// 解析文本节点
compileText(node) {
const reg = /\{\{(.*?)\}\}/;
const match = node.textContent.match(reg);
if (match) {
const key = match[1].trim();
node.textContent = this.$data[key];
}
}
}

3.2 测试模板解析

index.html 中测试模板解析功能:

1
2
3
4
<div id="app">
<p>{{ message }}</p>
<input v-model="message" />
</div>

4. 双向数据绑定

4.1 实现 v-model

compileElement 方法中已经实现了 v-model 的双向绑定功能。

4.2 测试双向绑定

index.html 中测试双向绑定功能:

1
2
3
4
<div id="app">
<p>{{ message }}</p>
<input v-model="message" />
</div>

5. 整合与测试

5.1 完整代码

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
class MiniVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
this.compile(document.querySelector(options.el));
}

// 数据劫持
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}

// 定义响应式
defineReactive(obj, key, val) {
this.observe(val); // 递归处理嵌套对象
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
console.log(`属性 ${key} 更新为 ${newVal}`);
}
});
}

// 模板解析
compile(el) {
const nodes = el.childNodes;
nodes.forEach(node => {
if (node.nodeType === 1) { // 元素节点
this.compileElement(node);
} else if (node.nodeType === 3) { // 文本节点
this.compileText(node);
}
});
}

// 解析元素节点
compileElement(node) {
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-model') {
const key = attr.value;
node.value = this.$data[key];
node.addEventListener('input', () => {
this.$data[key] = node.value;
});
}
});
}

// 解析文本节点
compileText(node) {
const reg = /\{\{(.*?)\}\}/;
const match = node.textContent.match(reg);
if (match) {
const key = match[1].trim();
node.textContent = this.$data[key];
}
}
}

5.2 测试

在浏览器中打开 index.html,输入框的值会实时更新到 p 标签中,同时控制台会输出数据更新的日志。


总结

通过以上步骤,我们可以实现一个简约版的 Vue.js,包含了响应式系统、模板解析和双向数据绑定功能。实现虽然简单,但涵盖了 Vue 的核心思想。