目标
实现一个简约版的 Vue.js,包含以下核心功能:
- 响应式系统:通过数据劫持实现数据的响应式更新。
- 模板解析:解析
{{}}
语法,将数据绑定到视图。
- 双向数据绑定:通过
v-model
实现表单元素与数据的双向绑定。
实现方案表
1. 项目初始化
- 创建一个空项目,初始化
package.json
。
- 创建入口文件
index.html
和主逻辑文件 mini-vue.js
。
2. 响应式系统
- 实现数据劫持,监听数据的变化。
- 使用
Object.defineProperty
或 Proxy
实现数据的 getter
和 setter
。
- 实现依赖收集和派发更新机制。
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!';
|
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 的核心思想。