<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue双向绑定原理演示</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5',
secondary: '#6366F1',
accent: '#818CF8',
neutral: '#F3F4F6',
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.transition-all-300 {
transition: all 0.3s ease;
}
.bg-gradient-primary {
background: linear-gradient(135deg, #4F46E5 0%, #818CF8 100%);
}
}
</style>
</head>
<body class="font-inter bg-gray-50 min-h-screen flex flex-col">
<header class="bg-gradient-primary text-white shadow-lg">
<div class="container mx-auto px-4 py-6">
<h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold flex items-center">
<i class="fa fa-code mr-3"></i>Vue 双向绑定原理演示
</h1>
<p class="mt-2 text-white/80 text-lg">使用原生 JavaScript 实现数据与视图的双向绑定</p>
</div>
</header>
<main class="flex-grow container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- 左侧:双向绑定演示 -->
<div class="bg-white rounded-xl shadow-xl p-6 transition-all-300 hover:shadow-2xl">
<h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
<i class="fa fa-magic text-primary mr-2"></i>双向绑定演示
</h2>
<div class="space-y-6">
<div class="form-group">
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">你的名字</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
<i class="fa fa-user"></i>
</span>
<input type="text" id="name" class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all-300" placeholder="请输入你的名字">
</div>
</div>
<div class="form-group">
<label for="message" class="block text-sm font-medium text-gray-700 mb-1">你的留言</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
<i class="fa fa-comment"></i>
</span>
<textarea id="message" rows="3" class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all-300" placeholder="请输入你的留言"></textarea>
</div>
</div>
<div class="bg-neutral rounded-lg p-4 border border-gray-200">
<h3 class="font-medium text-gray-700 mb-2">预览效果</h3>
<div id="preview" class="text-gray-600">
<p>你好,<span id="nameDisplay" class="font-medium text-primary">-</span></p>
<p class="mt-2">你的留言是:<span id="messageDisplay" class="italic text-gray-700">-</span></p>
</div>
</div>
</div>
</div>
<!-- 右侧:双向绑定实现原理 -->
<div class="bg-white rounded-xl shadow-xl p-6 transition-all-300 hover:shadow-2xl">
<h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
<i class="fa fa-code-fork text-primary mr-2"></i>实现原理
</h2>
<div class="space-y-4">
<div class="p-4 bg-blue-50 border-l-4 border-primary rounded-r">
<h3 class="font-medium text-primary">1. 数据劫持</h3>
<p class="text-gray-700 mt-1">使用 Object.defineProperty() 来劫持数据的 getter 和 setter</p>
</div>
<div class="p-4 bg-blue-50 border-l-4 border-primary rounded-r">
<h3 class="font-medium text-primary">2. 发布-订阅模式</h3>
<p class="text-gray-700 mt-1">每个数据属性都有一个 dep(依赖)对象,用于收集依赖和发布更新</p>
</div>
<div class="p-4 bg-blue-50 border-l-4 border-primary rounded-r">
<h3 class="font-medium text-primary">3. 观察者</h3>
<p class="text-gray-700 mt-1">Watcher 对象会订阅数据变化,并更新对应的 DOM 元素</p>
</div>
<div class="p-4 bg-blue-50 border-l-4 border-primary rounded-r">
<h3 class="font-medium text-primary">4. 解析器</h3>
<p class="text-gray-700 mt-1">Compile 对象解析 DOM 中的指令和插值表达式,初始化视图</p>
</div>
</div>
<div class="mt-6">
<h3 class="font-medium text-gray-800 mb-3">核心代码</h3>
<div class="bg-gray-800 text-white p-4 rounded-lg overflow-x-auto text-sm font-mono">
<pre>
// 实现数据劫持
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 递归劫持子属性
});
}
defineReactive(obj, key, value) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
dep.notify(); // 通知所有订阅者
}
});
}
}
// 发布者-订阅者模式
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 订阅者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldValue = this.getOldValue();
}
getOldValue() {
Dep.target = this;
const oldValue = compileUtil.getValue(this.expr, this.vm);
Dep.target = null;
return oldValue;
}
update() {
const newValue = compileUtil.getValue(this.expr, this.vm);
if (newValue !== this.oldValue) {
this.cb(newValue);
}
}
}
</pre>
</div>
</div>
</div>
</div>
<div class="mt-8 bg-white rounded-xl shadow-xl p-6">
<h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
<i class="fa fa-info-circle text-primary mr-2"></i>关于此演示
</h2>
<div class="prose max-w-none">
<p>这个演示展示了 Vue.js 双向数据绑定的核心原理,通过原生 JavaScript 实现了以下功能:</p>
<ul class="list-disc pl-5 mt-2">
<li>数据劫持:使用 Object.defineProperty() 监听数据变化</li>
<li>发布-订阅模式:实现依赖收集和通知机制</li>
<li>DOM 更新:当数据变化时自动更新视图</li>
<li>事件监听:当视图变化时自动更新数据</li>
</ul>
<p class="mt-3">实际的 Vue.js 框架比这个示例复杂得多,它还包含虚拟 DOM、组件系统、路由等高级功能。这个演示只是为了帮助理解双向绑定的基本原理。</p>
</div>
</div>
</main>
<footer class="bg-gray-800 text-white py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<p>© 2025 Vue 双向绑定原理演示</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-white transition-all-300">
<i class="fa fa-github"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-all-300">
<i class="fa fa-twitter"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-all-300">
<i class="fa fa-linkedin"></i>
</a>
</div>
</div>
</div>
</footer>
<script>
// 实现一个简单的 Vue 双向绑定
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$methods = options.methods;
// 数据代理,使得可以通过 vm.xxx 访问 vm.$data.xxx
this.proxyData(this.$data);
// 1. 数据劫持
new Observer(this.$data);
// 2. 编译模板
if (this.$el) {
new Compile(this.$el, this);
// 3. 初始化生命周期钩子
if (options.mounted) {
options.mounted.call(this);
}
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
});
}
}
// 数据劫持
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
// 递归劫持子属性
this.observe(data[key]);
});
}
defineReactive(obj, key, value) {
// 每个属性都有一个依赖收集器
const dep = new Dep();
// 递归劫持子属性
this.observe(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
if (newVal === value) {
return;
}
value = newVal;
// 新值可能是对象,需要继续劫持
this.observe(newVal);
// 通知所有订阅者
dep.notify();
}
});
}
}
// 依赖收集器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 订阅者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 保存旧值
this.oldValue = this.get();
}
get() {
// 将当前 watcher 实例保存到 Dep.target
Dep.target = this;
// 获取值的过程中会触发属性的 getter,从而收集依赖
const value = CompileUtil.getValue(this.expr, this.vm);
// 清空 Dep.target,防止重复收集
Dep.target = null;
return value;
}
update() {
const newValue = CompileUtil.getValue(this.expr, this.vm);
if (newValue !== this.oldValue) {
this.cb(newValue);
}
}
}
// 编译模板
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 1. 将真实 DOM 转换为文档片段
const fragment = this.node2Fragment(this.el);
// 2. 编译文档片段
this.compile(fragment);
// 3. 将编译后的文档片段添加回原 DOM
this.el.appendChild(fragment);
}
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 判断是否是事件指令
isEventDirective(dir) {
return dir.startsWith('on');
}
// 将 DOM 转换为文档片段
node2Fragment(el) {
const fragment = document.createDocumentFragment();
let firstChild;
// 将 DOM 元素的所有子节点移动到文档片段中
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
// 编译文档片段
compile(fragment) {
const childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 编译元素节点
this.compileElement(node);
// 递归编译子节点
this.compile(node);
} else {
// 编译文本节点
this.compileText(node);
}
});
}
// 编译元素节点
compileElement(node) {
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
if (this.isDirective(attrName)) {
// v-text, v-model, v-html 等
const expr = attr.value;
const dir = attrName.substring(2);
if (this.isEventDirective(dir)) {
// 事件指令,如 v-on:click
CompileUtil.eventHandler(node, this.vm, expr, dir);
} else {
// 普通指令
CompileUtil[dir] && CompileUtil[dir](node, this.vm, expr);
}
// 移除指令属性
node.removeAttribute(attrName);
}
});
}
// 编译文本节点
compileText(node) {
const text = node.textContent;
const reg = /\{\{([^}]+)\}\}/g;
if (reg.test(text)) {
CompileUtil.text(node, this.vm, text);
}
}
}
// 编译工具
const CompileUtil = {
// 获取表达式的值
getValue(expr, vm) {
return expr.split('.').reduce((data, current) => {
return data[current];
}, vm.$data);
},
// 设置表达式的值
setValue(expr, vm, inputVal) {
expr.split('.').reduce((data, current, index, arr) => {
if (index === arr.length - 1) {
data[current] = inputVal;
}
return data[current];
}, vm.$data);
},
// 处理 v-text 指令
text(node, vm, expr) {
let value;
if (expr.indexOf('{{') !== -1) {
// 处理插值表达式 {{xxx}}
value = expr.replace(/\{\{([^}]+)\}\}/g, (match, p1) => {
// 为每个插值表达式创建一个 watcher
new Watcher(vm, p1.trim(), (newValue) => {
this.updateText(node, newValue);
});
return this.getValue(p1.trim(), vm);
});
} else {
// 处理普通 v-text 指令
new Watcher(vm, expr, (newValue) => {
this.updateText(node, newValue);
});
value = this.getValue(expr, vm);
}
this.updateText(node, value);
},
// 更新文本内容
updateText(node, value) {
node.textContent = value;
},
// 处理 v-model 指令
model(node, vm, expr) {
// 设置初始值
const value = this.getValue(expr, vm);
// 为 input 元素添加事件监听
node.addEventListener('input', (e) => {
const newValue = e.target.value;
this.setValue(expr, vm, newValue);
});
// 创建 watcher,当数据变化时更新视图
new Watcher(vm, expr, (newValue) => {
node.value = newValue;
});
// 设置初始值
node.value = value;
},
// 处理事件指令
eventHandler(node, vm, expr, dir) {
const eventType = dir.split(':')[1];
const fn = vm.$methods && vm.$methods[expr];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm));
}
}
};
// 创建 Vue 实例
document.addEventListener('DOMContentLoaded', () => {
const vm = new Vue({
el: '#app',
data: {
name: '',
message: ''
},
methods: {
updatePreview() {
console.log('更新预览');
}
},
mounted() {
console.log('Vue 实例已挂载');
// 手动实现与表单元素的双向绑定
const nameInput = document.getElementById('name');
const messageInput = document.getElementById('message');
const nameDisplay = document.getElementById('nameDisplay');
const messageDisplay = document.getElementById('messageDisplay');
// 从数据到视图的绑定
new Watcher(vm, 'name', (newValue) => {
nameDisplay.textContent = newValue || '-';
});
new Watcher(vm, 'message', (newValue) => {
messageDisplay.textContent = newValue || '-';
});
// 从视图到数据的绑定
nameInput.addEventListener('input', (e) => {
vm.name = e.target.value;
});
messageInput.addEventListener('input', (e) => {
vm.message = e.target.value;
});
}
});
});
</script>
</body>
</html>
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。