目标
实现一个简约版的 Vue.js,包含以下功能:
- 响应式系统:通过数据劫持和分发订阅实现响应式。
- 模板解析:实现 {{}}模板语法,将数据绑定到视图。
- 双向数据绑定:通过 v-model实现表单元素与数据的双向绑定。
方案规划
1. 项目初始化
- 创建一个空项目,初始化 package.json。
- 创建入口文件 index.html和主逻辑文件mini-vue.js。
2. 响应式系统
- 实现数据劫持,监听数据的变化。
- 使用 Object.defineProperty实现数据的getter和setter。
- 实现依赖收集和派发更新机制。
3. 模板解析
- 实现 {{}}语法的解析,将数据绑定到 DOM。
- 使用正则表达式匹配 {{}}中的表达式,并替换为实际数据。
4. 双向数据绑定
- 实现 v-model指令,将表单元素的值与数据进行绑定。
- 监听表单元素的 input事件,更新数据;同时监听数据变化,更新表单元素的值。
动手
1. 项目初始化
1.1 创建项目
| 12
 3
 
 | mkdir mini-vuecd mini-vue
 npm init -y
 
 | 
1.2 创建文件
- index.html:用于测试 Vue 功能。
- mini-vue.js:实现 Vue 核心逻辑。
| 12
 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 类,并实现数据的响应式。
| 12
 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 中测试数据劫持功能:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | const app = new MiniVue({el: '#app',
 data: {
 message: 'Hello, Mini Vue!'
 }
 });
 
 
 app.$data.message = 'Hello, World!';
 
 
 | 
3. 模板解析
3.1 实现 双括号 解析
在 MiniVue 类中添加模板解析功能。
| 12
 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 中测试模板解析功能:
| 12
 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 中测试双向绑定功能:
| 12
 3
 4
 
 | <div id="app"><p>{{ message }}</p>
 <input v-model="message" />
 </div>
 
 | 
完整代码
| 12
 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];
 }
 }
 }
 
 |