实现一个迷你版的Vue
手写实现Vue的功能,包括响应式系统、模板编译、虚拟DOM和组件系统
问题
实现一个简化版的Vue框架,需要包含以下功能:
- 响应式数据系统(数据劫持)
- 模板编译(解析指令和插值表达式)
- 依赖收集和派发更新
- 视图更新机制
解答
// 依赖收集器
class Dep {
constructor() {
this.subscribers = new Set();
}
// 添加订阅者
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
// 通知所有订阅者
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
Dep.target = null;
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
this.value = this.get();
}
get() {
Dep.target = this;
// 触发getter,进行依赖收集
const value = this.vm.$data[this.key];
Dep.target = null;
return value;
}
update() {
const oldValue = this.value;
const newValue = this.vm.$data[this.key];
if (oldValue !== newValue) {
this.value = newValue;
this.callback.call(this.vm, newValue, oldValue);
}
}
}
// 响应式处理
class Observer {
constructor(data) {
this.data = data;
this.walk(data);
}
walk(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, value) {
const dep = new Dep();
// 递归处理嵌套对象
this.walk(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
dep.depend();
return value;
},
set(newValue) {
if (newValue === value) {
return;
}
value = newValue;
// 派发更新
dep.notify();
}
});
}
}
// 编译器
class Compiler {
constructor(el, vm) {
this.el = typeof el === 'string' ? document.querySelector(el) : el;
this.vm = vm;
if (this.el) {
// 将节点移入文档片段,减少回流
this.fragment = this.nodeToFragment(this.el);
// 编译模板
this.compile(this.fragment);
// 将编译后的内容放回页面
this.el.appendChild(this.fragment);
}
}
nodeToFragment(node) {
const fragment = document.createDocumentFragment();
let child;
while (child = node.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
compile(node) {
const childNodes = node.childNodes;
Array.from(childNodes).forEach(child => {
if (this.isElementNode(child)) {
// 编译元素节点
this.compileElement(child);
} else if (this.isTextNode(child)) {
// 编译文本节点
this.compileText(child);
}
// 递归编译子节点
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
});
}
compileElement(node) {
const attributes = node.attributes;
Array.from(attributes).forEach(attr => {
const { name, value } = attr;
// 处理 v-model 指令
if (name === 'v-model') {
this.modelUpdater(node, value);
}
// 处理 v-text 指令
else if (name === 'v-text') {
this.textUpdater(node, value);
}
// 处理 v-html 指令
else if (name === 'v-html') {
this.htmlUpdater(node, value);
}
// 处理 @click 事件
else if (name.startsWith('@')) {
const eventType = name.substring(1);
this.eventHandler(node, value, eventType);
}
});
}
compileText(node) {
const text = node.textContent;
const reg = /\{\{(.+?)\}\}/g;
if (reg.test(text)) {
const key = RegExp.$1.trim();
node.textContent = text.replace(reg, this.vm.$data[key]);
// 创建观察者,数据变化时更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = text.replace(reg, newValue);
});
}
}
modelUpdater(node, key) {
node.value = this.vm.$data[key];
// 数据变化更新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 视图变化更新数据
node.addEventListener('input', (e) => {
this.vm.$data[key] = e.target.value;
});
}
textUpdater(node, key) {
node.textContent = this.vm.$data[key];
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
htmlUpdater(node, key) {
node.innerHTML = this.vm.$data[key];
new Watcher(this.vm, key, (newValue) => {
node.innerHTML = newValue;
});
}
eventHandler(node, method, eventType) {
const fn = this.vm.$methods && this.vm.$methods[method];
if (fn) {
node.addEventListener(eventType, fn.bind(this.vm));
}
}
isElementNode(node) {
return node.nodeType === 1;
}
isTextNode(node) {
return node.nodeType === 3;
}
}
// MiniVue 主类
class MiniVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$methods = options.methods;
// 将data代理到实例上
this.proxyData(this.$data);
// 数据响应式处理
new Observer(this.$data);
// 编译模板
if (this.$el) {
new Compiler(this.$el, this);
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
}
});
});
}
}
使用示例
// HTML 模板
/*
<div id="app">
<h1>{{ title }}</h1>
<p v-text="message"></p>
<input type="text" v-model="inputValue">
<p>输入的值:{{ inputValue }}</p>
<button @click="handleClick">点击我</button>
<div v-html="htmlContent"></div>
</div>
*/
// 创建 MiniVue 实例
const vm = new MiniVue({
el: '#app',
data: {
title: '迷你版Vue',
message: '这是一个简化版的Vue实现',
inputValue: 'Hello',
htmlContent: '<strong>这是HTML内容</strong>'
},
methods: {
handleClick() {
this.message = '按钮被点击了!';
this.title = '标题已更新';
}
}
});
// 可以直接访问和修改数据
console.log(vm.title); // '迷你版Vue'
vm.inputValue = 'World'; // 视图会自动更新
关键点
-
响应式原理:使用
Object.defineProperty劫持数据的 getter 和 setter,在 getter 中收集依赖,在 setter 中触发更新 -
发布订阅模式:通过 Dep 类管理依赖,Watcher 类作为订阅者,实现数据变化时自动更新视图
-
依赖收集:通过全局变量
Dep.target标记当前正在执行的 Watcher,在数据被访问时建立依赖关系 -
模板编译:解析 DOM 节点,识别指令(v-model、v-text、v-html)和插值表达式({{ }}),建立数据与视图的绑定关系
-
双向绑定:v-model 指令通过监听 input 事件实现视图到数据的更新,通过 Watcher 实现数据到视图的更新
-
文档片段优化:使用
DocumentFragment减少 DOM 操作次数,提高性能 -
数据代理:将
$data中的属性代理到 Vue 实例上,可以通过this.xxx直接访问数据 -
递归处理:对嵌套对象进行递归响应式处理,对子节点进行递归编译
目录