JS案例-基于Proxy的响应式数据
基于Proxy的响应式数据系统实现原理解析
文章目录
- 基于Proxy的响应式数据系统实现原理解析
- 引言
- 响应式系统的基本原理
- Proxy API概述
- 响应式系统的核心实现
- 1. 创建响应式对象
- 2. 依赖追踪系统
- 3. 变更通知系统
- 4. 副作用函数系统
- 高级特性与边缘情况处理
- 1. 深层响应式
- 2. 数组的特殊处理
- 3. 新增属性的处理
- 实际应用示例
- 性能优化与最佳实践
- 1. 避免不必要的更新
- 2. 合理使用调度器
- 3. 使用WeakMap避免内存泄漏
- 总结
- 效果
- 完整代码
- HTML
- JS
- CSS
引言
响应式数据系统是现代前端框架的核心部分,特别是在Vue 3中,使用ES6的Proxy API重构了其响应式系统,实现了更高效、更全面的数据变化监测。本文将深入分析基于Proxy的响应式数据系统的实现原理,探讨其如何追踪数据变化并自动更新页面。
响应式系统的基本原理
响应式系统的核心思想是:当数据发生变化时,依赖于该数据的视图或计算属性应该自动更新。要实现这一机制,需要解决三个关键问题:
- 如何拦截数据的读写操作
- 如何收集数据与副作用函数之间的依赖关系
- 如何在数据变化时触发相应的副作用函数
在Vue 3中,这些问题通过基于Proxy的响应式系统得到了优雅的解决。
Proxy API概述
JavaScript的Proxy对象允许开发者创建一个对象的代理,从而能够拦截并自定义对该对象的基本操作,如属性查找、赋值、枚举、函数调用等。
const proxy = new Proxy(target, handler);
其中,target
是要被代理的目标对象,handler
是一个包含捕获器(trap)的对象,定义了对拦截操作的处理方法。
响应式系统的核心实现
1. 创建响应式对象
实现响应式对象的核心是使用Proxy拦截对象的属性访问和修改操作。以下是一个基本的响应式函数实现:
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. 依赖追踪系统
要建立数据与副作用函数的关联,需要一个精心设计的依赖追踪系统:
// 全局依赖映射表
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. 变更通知系统
当数据变化时,需要通知所有相关的副作用函数执行更新:
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更新的桥梁,它需要能够自动追踪依赖并在依赖变化时执行:
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. 深层响应式
响应式系统需要能够处理嵌套对象,确保对象的任何层级属性变化都能被检测到:
// 在get捕获器中处理嵌套对象
const value = Reflect.get(obj, key, receiver);
if (typeof value === 'object' && value !== null) {return reactive(value);
}
通过在获取属性值时递归地将对象转换为响应式,系统能够实现深层响应式,无论对象的结构有多复杂。
2. 数组的特殊处理
数组作为JavaScript中重要的数据结构,需要特殊处理:
// 数组长度变化的特殊处理
if (Array.isArray(target) && key === 'length') {depsMap.forEach((dep, key) => {if (key >= newValue) {add(dep);}});
}
当数组长度减小时,所有超出新长度的索引都需要触发更新,这确保了依赖于这些索引的视图能够正确更新。
3. 新增属性的处理
对象新增属性是响应式系统中的一个常见挑战。在Vue 2中,这需要使用特殊的Vue.set
方法,而在基于Proxy的系统中,这个问题得到了优雅的解决:
// 在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');
}
通过检查属性是否已存在,系统可以区分"新增属性"和"更新属性"的操作,并分别进行处理。
实际应用示例
以下是一个简单的应用示例,展示如何使用响应式系统构建一个交互式界面:
// 创建响应式状态
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. 避免不必要的更新
响应式系统应该只在值确实发生变化时才触发更新:
// 仅当值发生变化时才触发更新
if (oldValue !== value) {trigger(obj, key, oldValue, value, 'set');
}
这避免了在设置相同值时的不必要更新,提高了系统的效率。
2. 合理使用调度器
通过在effect
中支持自定义调度器,可以灵活控制副作用函数的执行时机和方式:
if (effect.options?.scheduler) {effect.options.scheduler(effect);
} else {effect();
}
这使得可以实现批量更新、异步更新等高级功能,避免不必要的中间状态渲染。
3. 使用WeakMap避免内存泄漏
依赖收集系统使用WeakMap
存储目标对象与依赖的关系:
const targetMap = new WeakMap();
这确保了当目标对象不再被引用时,相关的依赖信息也可以被垃圾回收,避免内存泄漏。
总结
基于Proxy的响应式数据系统是现代前端框架的重要基础设施,它通过巧妙地利用JavaScript的语言特性,实现了数据与UI之间的自动同步,大大简化了前端开发。本文分析的实现方式与Vue 3的核心响应式系统类似,展示了如何构建一个高效、灵活的响应式系统。
理解响应式系统的原理,不仅有助于更好地使用框架,也为自定义需求提供了可能性。随着Web应用的复杂度不断提高,响应式编程范式将继续发挥重要作用,而基于Proxy的实现方式也将成为这一领域的主流选择。
效果
基于Proxy的响应式数据
完整代码
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
/*** 响应式数据系统实现 - 基于 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 指向栈中下一个副作用函数,或者置为 nullactiveEffect = 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', () => {// 使用时间戳作为唯一IDconst 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
: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;}}