基于Proxy的响应式数据系统实现原理解析
文章目录
- 基于Proxy的响应式数据系统实现原理解析
-
- 引言
- 响应式系统的基本原理
- [Proxy API概述](#Proxy API概述)
- 响应式系统的核心实现
-
- [1. 创建响应式对象](#1. 创建响应式对象)
- [2. 依赖追踪系统](#2. 依赖追踪系统)
- [3. 变更通知系统](#3. 变更通知系统)
- [4. 副作用函数系统](#4. 副作用函数系统)
- 高级特性与边缘情况处理
-
- [1. 深层响应式](#1. 深层响应式)
- [2. 数组的特殊处理](#2. 数组的特殊处理)
- [3. 新增属性的处理](#3. 新增属性的处理)
- 实际应用示例
- 性能优化与最佳实践
-
- [1. 避免不必要的更新](#1. 避免不必要的更新)
- [2. 合理使用调度器](#2. 合理使用调度器)
- [3. 使用WeakMap避免内存泄漏](#3. 使用WeakMap避免内存泄漏)
- 总结
- 效果
- 完整代码
引言
响应式数据系统是现代前端框架的核心部分,特别是在Vue 3中,使用ES6的Proxy API重构了其响应式系统,实现了更高效、更全面的数据变化监测。本文将深入分析基于Proxy的响应式数据系统的实现原理,探讨其如何追踪数据变化并自动更新页面。
响应式系统的基本原理
响应式系统的核心思想是:当数据发生变化时,依赖于该数据的视图或计算属性应该自动更新。要实现这一机制,需要解决三个关键问题:
- 如何拦截数据的读写操作
- 如何收集数据与副作用函数之间的依赖关系
- 如何在数据变化时触发相应的副作用函数
在Vue 3中,这些问题通过基于Proxy的响应式系统得到了优雅的解决。
Proxy API概述
JavaScript的Proxy对象允许开发者创建一个对象的代理,从而能够拦截并自定义对该对象的基本操作,如属性查找、赋值、枚举、函数调用等。
javascript
const proxy = new Proxy(target, handler);
其中,target
是要被代理的目标对象,handler
是一个包含捕获器(trap)的对象,定义了对拦截操作的处理方法。
响应式系统的核心实现
1. 创建响应式对象
实现响应式对象的核心是使用Proxy拦截对象的属性访问和修改操作。以下是一个基本的响应式函数实现:
javascript
function reactive(target) {
// 避免重复包装已经是响应式的对象
if (isReactive(target)) {
return target;
}
const handler = {
get(obj, key, receiver) {
// 用于检查对象是否为响应式
if (key === '__isReactive') {
return true;
}
// 对特殊属性的处理
if (typeof key === 'symbol' || key === '__proto__') {
return Reflect.get(obj, key, receiver);
}
// 依赖追踪
track(obj, key);
const value = Reflect.get(obj, key, receiver);
// 深层响应式处理
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(obj, key, value, receiver) {
const oldValue = Reflect.get(obj, key, receiver);
const hadKey = Object.prototype.hasOwnProperty.call(obj, key);
const result = Reflect.set(obj, key, value, receiver);
// 确定是新增属性还是更新已有属性
if (!hadKey) {
trigger(obj, key, undefined, value, 'add');
} else if (oldValue !== value) {
trigger(obj, key, oldValue, value, 'set');
}
return result;
},
deleteProperty(obj, key) {
const hadKey = Object.prototype.hasOwnProperty.call(obj, key);
const result = Reflect.deleteProperty(obj, key);
if (hadKey && result) {
trigger(obj, key, undefined, undefined, 'delete');
}
return result;
}
};
return new Proxy(target, handler);
}
function isReactive(obj) {
return obj && obj.__isReactive === true;
}
这个reactive
函数实现了三个主要功能:
- 通过
get
捕获器拦截属性读取操作,进行依赖追踪 - 通过
set
捕获器拦截属性设置操作,在属性值变化时触发更新 - 通过
deleteProperty
捕获器拦截属性删除操作,触发相关更新
2. 依赖追踪系统
要建立数据与副作用函数的关联,需要一个精心设计的依赖追踪系统:
javascript
// 全局依赖映射表
const targetMap = new WeakMap();
let activeEffect = null;
let effectStack = [];
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
}
这个追踪系统使用了一个多层嵌套的数据结构:
WeakMap
以目标对象为键,值是一个Map
- 内层
Map
以属性名为键,值是一个Set
集合 Set
集合中存储了所有依赖于该属性的副作用函数
通过这种结构,系统能够精确地记录"哪个对象的哪个属性被哪些副作用函数使用",从而在属性变化时只触发相关的副作用函数。
3. 变更通知系统
当数据变化时,需要通知所有相关的副作用函数执行更新:
javascript
function trigger(target, key, oldValue, newValue, type = 'set') {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect));
}
};
// 添加特定属性的依赖项
add(depsMap.get(key));
// 对象新增或删除属性时,触发与整个对象相关的依赖
if (type === 'add' || type === 'delete') {
// 添加对象本身的依赖项(通常与迭代器相关)
const iterationKey = Array.isArray(target) ? 'length' : Symbol.iterator;
add(depsMap.get(iterationKey));
// 特别处理通用依赖收集
add(depsMap.get(Symbol('iterate')));
}
// 数组特殊处理
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((dep, key) => {
if (key >= newValue) {
add(dep);
}
});
}
// 执行所有收集到的副作用函数
effects.forEach(effect => {
if (effect.options?.scheduler) {
effect.options.scheduler(effect);
} else {
effect();
}
});
}
trigger
函数处理了多种更新场景:
- 属性值的更新触发直接相关的副作用函数
- 对象添加或删除属性时,触发与对象迭代相关的副作用函数
- 数组长度变化时,触发与受影响索引相关的副作用函数
4. 副作用函数系统
副作用函数是连接响应式数据与UI更新的桥梁,它需要能够自动追踪依赖并在依赖变化时执行:
javascript
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
const result = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
return result;
};
effectFn.deps = [];
effectFn.options = options;
if (!options.lazy) {
effectFn();
}
return effectFn;
}
function cleanup(effect) {
for (let i = 0; i < effect.deps.length; i++) {
const dep = effect.deps[i];
dep.delete(effect);
}
effect.deps.length = 0;
}
effect
函数实现了以下功能:
- 包装原始函数,使其成为响应式的
- 在执行过程中自动追踪依赖
- 支持清理旧的依赖关系,避免不必要的更新
- 支持嵌套调用,通过
effectStack
管理当前激活的副作用函数
高级特性与边缘情况处理
1. 深层响应式
响应式系统需要能够处理嵌套对象,确保对象的任何层级属性变化都能被检测到:
javascript
// 在get捕获器中处理嵌套对象
const value = Reflect.get(obj, key, receiver);
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
通过在获取属性值时递归地将对象转换为响应式,系统能够实现深层响应式,无论对象的结构有多复杂。
2. 数组的特殊处理
数组作为JavaScript中重要的数据结构,需要特殊处理:
javascript
// 数组长度变化的特殊处理
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((dep, key) => {
if (key >= newValue) {
add(dep);
}
});
}
当数组长度减小时,所有超出新长度的索引都需要触发更新,这确保了依赖于这些索引的视图能够正确更新。
3. 新增属性的处理
对象新增属性是响应式系统中的一个常见挑战。在Vue 2中,这需要使用特殊的Vue.set
方法,而在基于Proxy的系统中,这个问题得到了优雅的解决:
javascript
// 在set捕获器中区分新增和更新
const hadKey = Object.prototype.hasOwnProperty.call(obj, key);
if (!hadKey) {
trigger(obj, key, undefined, value, 'add');
} else if (oldValue !== value) {
trigger(obj, key, oldValue, value, 'set');
}
通过检查属性是否已存在,系统可以区分"新增属性"和"更新属性"的操作,并分别进行处理。
实际应用示例
以下是一个简单的应用示例,展示如何使用响应式系统构建一个交互式界面:
javascript
// 创建响应式状态
const state = reactive({
count: 0,
progress: 0,
properties: {}
});
// DOM初始化后创建响应式效果
document.addEventListener('DOMContentLoaded', () => {
const countValue = document.getElementById('count-value');
const progressFill = document.getElementById('progress-fill');
const progressValue = document.getElementById('progress-value');
const dataList = document.getElementById('data-list');
// 监视计数器变化
effect(() => {
countValue.textContent = state.count;
state.progress = Math.min(Math.max(state.count, 0), 100);
});
// 监视进度条变化
effect(() => {
progressFill.style.width = `${state.progress}%`;
progressValue.textContent = state.progress;
});
// 监视属性列表变化
effect(() => {
dataList.innerHTML = '';
Object.entries(state.properties).forEach(([key, value]) => {
const item = document.createElement('div');
item.classList.add('data-item');
item.innerHTML = `
<span class="property-name">${key}</span>
<span class="property-value">${value}</span>
`;
dataList.appendChild(item);
});
});
// 绑定事件处理函数
document.getElementById('increment').addEventListener('click', () => {
state.count++;
});
document.getElementById('add-property').addEventListener('click', () => {
const id = Date.now();
const key = `prop_${id}`;
const value = Math.floor(Math.random() * 1000);
state.properties[key] = value;
});
});
在这个示例中,声明式地定义了数据与UI之间的关系,当用户交互导致数据变化时,相关的UI部分会自动更新,无需手动操作DOM。
性能优化与最佳实践
1. 避免不必要的更新
响应式系统应该只在值确实发生变化时才触发更新:
javascript
// 仅当值发生变化时才触发更新
if (oldValue !== value) {
trigger(obj, key, oldValue, value, 'set');
}
这避免了在设置相同值时的不必要更新,提高了系统的效率。
2. 合理使用调度器
通过在effect
中支持自定义调度器,可以灵活控制副作用函数的执行时机和方式:
javascript
if (effect.options?.scheduler) {
effect.options.scheduler(effect);
} else {
effect();
}
这使得可以实现批量更新、异步更新等高级功能,避免不必要的中间状态渲染。
3. 使用WeakMap避免内存泄漏
依赖收集系统使用WeakMap
存储目标对象与依赖的关系:
javascript
const targetMap = new WeakMap();
这确保了当目标对象不再被引用时,相关的依赖信息也可以被垃圾回收,避免内存泄漏。
总结
基于Proxy的响应式数据系统是现代前端框架的重要基础设施,它通过巧妙地利用JavaScript的语言特性,实现了数据与UI之间的自动同步,大大简化了前端开发。本文分析的实现方式与Vue 3的核心响应式系统类似,展示了如何构建一个高效、灵活的响应式系统。
理解响应式系统的原理,不仅有助于更好地使用框架,也为自定义需求提供了可能性。随着Web应用的复杂度不断提高,响应式编程范式将继续发挥重要作用,而基于Proxy的实现方式也将成为这一领域的主流选择。
效果
基于Proxy的响应式数据
完整代码
HTML
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="刘大大">
<meta name="date" content="2025-04-19">
<!-- 作者: 刘大大 | 日期: 2025年4月19日 -->
<title>响应式数据系统示例</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<header>
<h1>响应式数据监控面板</h1>
</header>
<section class="dashboard">
<div class="card" id="counter-card">
<h2>计数器</h2>
<div class="value-display">
<span id="count-value">0</span>
</div>
<div class="controls">
<button id="decrement">-</button>
<button id="increment">+</button>
<button id="reset">重置</button>
</div>
</div>
<div class="card" id="progress-card">
<h2>进度指示器</h2>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<p class="progress-text">当前进度: <span id="progress-value">0</span>%</p>
</div>
<div class="card" id="data-card">
<h2>数据属性</h2>
<div class="data-list" id="data-list">
<!-- 动态生成的数据属性列表 -->
</div>
<button id="add-property">添加随机属性</button>
</div>
</section>
<section class="action-log">
<h2>操作日志</h2>
<div class="log-container" id="log-container">
<!-- 日志内容将在这里动态生成 -->
</div>
<button id="clear-log">清除日志</button>
</section>
</div>
<script src="reactive.js"></script>
</body>
</html>
JS
javascript
/**
* 响应式数据系统实现 - 基于 ES6 Proxy
* 类似 Vue3 的响应式核心实现
*
* 主要功能:
* 1. 数据响应式化 - 通过 Proxy 拦截对象的读写操作
* 2. 依赖收集 - 自动追踪谁在使用响应式数据
* 3. 变更通知 - 数据变化时自动通知依赖更新
* 4. 嵌套响应式 - 支持深层次的对象响应式
*/
/**
* 创建响应式对象
* @param {Object|Array} target - 需要被响应式化的目标对象
* @return {Proxy} - 返回包装后的响应式代理对象
*
* 核心原理:使用 ES6 的 Proxy 拦截对象的属性访问和修改操作,
* 在 get 时收集依赖,在 set 时触发更新。
*/
function reactive(target) {
// 避免重复包装已经是响应式的对象
if (isReactive(target)) {
return target;
}
// Proxy 处理器对象,定义拦截操作的行为
const handler = {
/**
* 拦截对象属性的读取操作
* @param {Object} obj - 原始对象
* @param {string|symbol} key - 属性名
* @param {any} receiver - 代理对象或继承自代理对象的对象
* @return {any} - 属性值
*
* 功能:
* 1. 拦截属性读取,追踪谁在读取该属性(依赖收集)
* 2. 对嵌套对象进行响应式转换(深层响应式)
*/
get(obj, key, receiver) {
// 用于检查对象是否为响应式
if (key === '__isReactive') {
return true;
}
// 对特殊属性的处理:Symbol 类型的键和 __proto__ 属性直接返回
// 这些属性通常不需要追踪和响应式处理
if (typeof key === 'symbol' || key === '__proto__') {
return Reflect.get(obj, key, receiver);
}
// 依赖追踪 - 记录当前属性被哪个副作用函数使用
track(obj, key);
// 获取属性值
const value = Reflect.get(obj, key, receiver);
// 深层响应式处理 - 如果属性值是对象,则将其转换为响应式对象
// 这确保了对象的任何层级属性变化都能被检测到
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
/**
* 拦截对象属性的设置操作
* @param {Object} obj - 原始对象
* @param {string|symbol} key - 属性名
* @param {any} value - 新的属性值
* @param {any} receiver - 代理对象或继承自代理对象的对象
* @return {boolean} - 是否设置成功
*
* 功能:
* 1. 设置属性值
* 2. 如果值发生变化,触发依赖更新
* 3. 记录操作到日志
*/
set(obj, key, value, receiver) {
// 获取旧值,用于比较和日志记录
const oldValue = Reflect.get(obj, key, receiver);
const hadKey = Object.prototype.hasOwnProperty.call(obj, key);
// 设置新值
const result = Reflect.set(obj, key, value, receiver);
// 确定是新增属性还是更新已有属性
if (!hadKey) {
// 新增属性,无需比较旧值
trigger(obj, key, undefined, value, 'add');
} else if (oldValue !== value) {
// 更新已有属性,且值发生变化
trigger(obj, key, oldValue, value, 'set');
}
return result;
},
/**
* 拦截对象属性的删除操作
* @param {Object} obj - 原始对象
* @param {string|symbol} key - 要删除的属性名
* @return {boolean} - 是否删除成功
*
* 功能:
* 1. 删除属性
* 2. 如果属性确实存在并被删除,触发依赖更新
* 3. 记录删除操作到日志
*/
deleteProperty(obj, key) {
// 检查属性是否存在
const hadKey = Object.prototype.hasOwnProperty.call(obj, key);
// 执行删除操作
const result = Reflect.deleteProperty(obj, key);
// 如果属性存在且成功删除,则触发更新
if (hadKey && result) {
trigger(obj, key, undefined, undefined, 'delete');
}
return result;
}
};
// 创建并返回代理对象
return new Proxy(target, handler);
}
// ----------------- 依赖追踪系统 -----------------
/**
* 全局依赖映射表:存储所有响应式对象的依赖关系
*
* 数据结构:
* WeakMap<target, Map<key, Set<effect>>>
* 1. WeakMap:键是被代理的原始对象,弱引用不阻止垃圾回收
* 2. Map:键是对象的属性名
* 3. Set:存储依赖于该属性的所有副作用函数
*/
const targetMap = new WeakMap();
/**
* 当前正在执行的副作用函数,用于依赖收集
* 在 effect() 执行时设置,执行完后清除
*/
let activeEffect = null;
/**
* 副作用函数栈,用于处理嵌套的 effect 调用
* 例如当一个 effect 内部调用了另一个 effect 时,需要正确追踪依赖关系
*/
let effectStack = [];
/**
* 依赖追踪函数 - 在属性被读取时调用
* @param {Object} target - 原始对象
* @param {string|symbol} key - 属性名
*
* 功能:记录当前属性被哪个副作用函数使用,建立属性与副作用函数的依赖关系
*/
function track(target, key) {
// 如果没有活动的副作用函数,则不需要追踪
if (activeEffect) {
// 获取对象的依赖映射,如果不存在则创建
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取特定属性的依赖集合,如果不存在则创建
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 将当前活动的副作用函数添加到依赖集合中
// 同时在副作用函数上记录它依赖的集合,用于清理
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
}
/**
* 检查一个对象是否为响应式对象
* @param {Object} obj - 要检查的对象
* @return {boolean} - 如果对象是响应式的,返回true
*/
function isReactive(obj) {
return obj && obj.__isReactive === true;
}
/**
* 触发更新函数 - 在属性被修改时调用
* @param {Object} target - 原始对象
* @param {string|symbol} key - 属性名
* @param {any} oldValue - 旧值
* @param {any} newValue - 新值
* @param {string} type - 操作类型('set'、'add'或'delete')
*
* 功能:通知所有依赖于该属性的副作用函数执行更新
*/
function trigger(target, key, oldValue, newValue, type = 'set') {
// 获取对象的依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 创建一个新的 Set 来存储要执行的副作用函数
// 避免在遍历执行过程中修改原始集合导致的问题
const effects = new Set();
// 辅助函数:将依赖集合中的副作用函数添加到待执行集合
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect));
}
};
// 添加特定属性的依赖项
add(depsMap.get(key));
// 对象新增或删除属性时,触发与整个对象相关的依赖
// 这是为了处理遍历操作(如Object.keys, for...in等)的依赖
if (type === 'add' || type === 'delete') {
// 添加对象本身的依赖项(通常与迭代器相关)
const iterationKey = Array.isArray(target) ? 'length' : Symbol.iterator;
add(depsMap.get(iterationKey));
// 特别处理通用依赖收集,使用特殊符号标记整个对象的依赖
add(depsMap.get(Symbol('iterate')));
}
// 数组特殊处理:当数组长度变化时,可能影响索引相关的依赖
if (Array.isArray(target) && key === 'length') {
depsMap.forEach((dep, key) => {
// 当数组长度减小时,所有超出新长度的索引都需要触发更新
if (key >= newValue) {
add(dep);
}
});
}
// 记录操作到日志,展示在界面上
logOperation(key, oldValue, newValue, type);
// 执行所有收集到的副作用函数
effects.forEach(effect => {
// 如果副作用函数有自定义调度器,则使用调度器执行
// 调度器可以控制副作用函数的执行时机和方式
if (effect.options?.scheduler) {
effect.options.scheduler(effect);
} else {
// 否则直接执行副作用函数
effect();
}
});
}
/**
* 创建响应式副作用函数
* @param {Function} fn - 原始函数
* @param {Object} options - 选项(如lazy, scheduler等)
* @return {Function} - 增强后的副作用函数
*
* 功能:
* 1. 包装原始函数,使其成为响应式的
* 2. 在执行过程中自动追踪依赖
* 3. 支持清理旧的依赖关系
* 4. 支持嵌套调用
*/
function effect(fn, options = {}) {
// 创建副作用函数
const effectFn = () => {
// 清理之前的依赖关系,避免不必要的更新
cleanup(effectFn);
// 设置当前活动的副作用函数
activeEffect = effectFn;
// 压入副作用函数栈,支持嵌套调用
effectStack.push(effectFn);
// 执行原始函数,此过程中会触发代理的 get 操作,从而收集依赖
const result = fn();
// 恢复之前的状态
effectStack.pop();
// 将 activeEffect 指向栈中下一个副作用函数,或者置为 null
activeEffect = effectStack[effectStack.length - 1] || null;
return result;
};
// 存储副作用函数所依赖的依赖集合
effectFn.deps = [];
// 保存选项
effectFn.options = options;
// 如果不是惰性的,则立即执行
if (!options.lazy) {
effectFn();
}
return effectFn;
}
/**
* 清理副作用函数的依赖关系
* @param {Function} effect - 副作用函数
*
* 功能:从所有依赖集合中移除该副作用函数,
* 避免不必要的更新和内存泄漏
*/
function cleanup(effect) {
// 遍历副作用函数的所有依赖集合
for (let i = 0; i < effect.deps.length; i++) {
const dep = effect.deps[i];
// 从集合中移除该副作用函数
dep.delete(effect);
}
// 清空依赖数组
effect.deps.length = 0;
}
// ----------------- UI 交互与日志系统 -----------------
/**
* 操作日志记录函数
* @param {string|symbol} key - 属性名
* @param {any} oldValue - 旧值
* @param {any} newValue - 新值
* @param {string} type - 操作类型
*
* 功能:将数据变化记录到操作日志区域,便于观察数据变化
*/
function logOperation(key, oldValue, newValue, type) {
const logContainer = document.getElementById('log-container');
const now = new Date().toLocaleTimeString();
let logClass = 'updated';
let message = '';
// 简化对象表示的辅助函数
const simplifyValue = (value) => {
if (value === undefined) return 'undefined';
if (value === null) return 'null';
if (typeof value === 'object') {
// 如果是对象,返回简化的表示
if (Array.isArray(value)) {
return `数组[${value.length}]`;
} else {
// 对于对象,显示键的数量
const keys = Object.keys(value);
return `对象{${keys.length}属性: ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`;
}
}
return String(value);
};
// 根据操作类型生成不同的日志消息
switch (type) {
case 'add':
// 添加新属性
message = `[${now}] 添加属性: ${key} = ${simplifyValue(newValue)}`;
logClass = 'added';
break;
case 'set':
if (oldValue === undefined) {
// 添加新属性(向后兼容)
message = `[${now}] 添加属性: ${key} = ${simplifyValue(newValue)}`;
logClass = 'added';
} else {
// 更新已有属性
message = `[${now}] 更新属性: ${key} = ${simplifyValue(newValue)} (原值: ${simplifyValue(oldValue)})`;
}
break;
case 'delete':
// 删除属性
message = `[${now}] 删除属性: ${key}`;
logClass = 'reset';
break;
default:
// 其他操作
message = `[${now}] 操作: ${type} ${key}`;
}
// 创建日志条目并添加到日志容器
const logEntry = document.createElement('div');
logEntry.classList.add('log-entry', logClass);
logEntry.textContent = message;
logContainer.appendChild(logEntry);
// 自动滚动到最新日志
logContainer.scrollTop = logContainer.scrollHeight;
}
// ----------------- 应用状态与初始化 -----------------
/**
* 创建应用的响应式状态对象
* 包含计数器值、进度值和动态属性集合
*/
const state = reactive({
count: 0, // 计数器值
progress: 0, // 进度条值 (0-100)
properties: {} // 动态属性集合
});
/**
* DOM 加载完成后的初始化
* 1. 获取 DOM 元素引用
* 2. 创建响应式效果
* 3. 绑定事件处理函数
*/
document.addEventListener('DOMContentLoaded', () => {
// ---------- 获取 DOM 元素引用 ----------
// 计数器相关元素
const countValue = document.getElementById('count-value');
const incrementBtn = document.getElementById('increment');
const decrementBtn = document.getElementById('decrement');
const resetBtn = document.getElementById('reset');
// 进度条相关元素
const progressFill = document.getElementById('progress-fill');
const progressValue = document.getElementById('progress-value');
// 数据属性相关元素
const dataList = document.getElementById('data-list');
const addPropertyBtn = document.getElementById('add-property');
// 日志相关元素
const clearLogBtn = document.getElementById('clear-log');
// ---------- 创建响应式效果 ----------
/**
* 监视计数器变化的响应式效果
* 1. 更新计数器显示
* 2. 同步更新进度值(限制在0-100范围内)
*/
effect(() => {
// 更新计数器显示
countValue.textContent = state.count;
// 同步更新进度值 (0-100 范围内)
// 使用 Math.min 和 Math.max 限制值的范围
state.progress = Math.min(Math.max(state.count, 0), 100);
});
/**
* 监视进度条变化的响应式效果
* 1. 更新进度条填充宽度
* 2. 更新进度数值显示
*/
effect(() => {
// 更新进度条填充宽度
progressFill.style.width = `${state.progress}%`;
// 更新进度数值显示
progressValue.textContent = state.progress;
});
/**
* 监视属性列表变化的响应式效果
* 重新生成属性列表的 DOM 元素
*/
effect(() => {
// 清空现有列表
dataList.innerHTML = '';
// 遍历所有属性,为每个属性创建一个列表项
Object.entries(state.properties).forEach(([key, value]) => {
const item = document.createElement('div');
item.classList.add('data-item');
item.innerHTML = `
<span class="property-name">${key}</span>
<span class="property-value">${value}</span>
`;
dataList.appendChild(item);
});
});
// ---------- 绑定事件处理函数 ----------
// 增加计数器值
incrementBtn.addEventListener('click', () => {
state.count++;
});
// 减少计数器值
decrementBtn.addEventListener('click', () => {
state.count--;
});
// 重置所有状态
resetBtn.addEventListener('click', () => {
// 重置计数器和进度值
state.count = 0;
state.progress = 0;
// 清空属性集合
state.properties = {};
// 添加重置日志
const logEntry = document.createElement('div');
logEntry.classList.add('log-entry', 'reset');
logEntry.textContent = `[${new Date().toLocaleTimeString()}] 系统已重置`;
document.getElementById('log-container').appendChild(logEntry);
});
// 添加随机属性
addPropertyBtn.addEventListener('click', () => {
// 使用时间戳作为唯一ID
const id = Date.now();
// 创建属性名
const key = `prop_${id}`;
// 生成0-999之间的随机值
const value = Math.floor(Math.random() * 1000);
console.log('添加随机属性:', key, value);
// 替换整个对象 - 如果上面的方法不起作用,可以尝试这种方式
const newProperties = Object.assign({}, state.properties);
newProperties[key] = value;
state.properties = newProperties;
});
// 清除日志
clearLogBtn.addEventListener('click', () => {
document.getElementById('log-container').innerHTML = '';
});
});
CSS
css
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--accent-color: #e74c3c;
--text-color: #2c3e50;
--background-color: #ecf0f1;
--card-color: white;
--border-radius: 8px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.app-container {
/* max-width: 1200px; */
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
color: var(--primary-color);
font-size: 2.5rem;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background-color: var(--card-color);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--box-shadow);
transition: var(--transition);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: var(--primary-color);
margin-bottom: 1rem;
border-bottom: 2px solid var(--background-color);
padding-bottom: 0.5rem;
}
.value-display {
font-size: 3rem;
text-align: center;
margin: 1rem 0;
font-weight: bold;
color: var(--primary-color);
}
.controls {
display: flex;
justify-content: center;
gap: 0.5rem;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
font-size: 1rem;
}
button:hover {
background-color: #2980b9;
transform: scale(1.05);
}
button#reset {
background-color: var(--accent-color);
}
button#reset:hover {
background-color: #c0392b;
}
.progress-bar {
height: 20px;
background-color: var(--background-color);
border-radius: 10px;
margin: 1rem 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--secondary-color);
width: 0%;
transition: width 0.3s ease-in-out;
}
.progress-text {
text-align: center;
}
.data-list {
margin: 1rem 0;
}
.data-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
border-bottom: 1px solid var(--background-color);
}
.data-item:last-child {
border-bottom: none;
}
.action-log {
background-color: var(--card-color);
border-radius: var(--border-radius);
padding: 1.5rem;
box-shadow: var(--box-shadow);
}
.log-container {
height: 200px;
overflow-y: auto;
margin: 1rem 0;
padding: 1rem;
background-color: var(--background-color);
border-radius: var(--border-radius);
}
.log-entry {
padding: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.added {
color: var(--secondary-color);
}
.log-entry.updated {
color: var(--primary-color);
}
.log-entry.reset {
color: var(--accent-color);
}
#clear-log {
background-color: #7f8c8d;
}
#clear-log:hover {
background-color: #95a5a6;
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
.app-container {
padding: 1rem;
}
}