JS案例-基于Proxy的响应式数据

基于Proxy的响应式数据系统实现原理解析

文章目录

引言

响应式数据系统是现代前端框架的核心部分,特别是在Vue 3中,使用ES6的Proxy API重构了其响应式系统,实现了更高效、更全面的数据变化监测。本文将深入分析基于Proxy的响应式数据系统的实现原理,探讨其如何追踪数据变化并自动更新页面。

响应式系统的基本原理

响应式系统的核心思想是:当数据发生变化时,依赖于该数据的视图或计算属性应该自动更新。要实现这一机制,需要解决三个关键问题:

  1. 如何拦截数据的读写操作
  2. 如何收集数据与副作用函数之间的依赖关系
  3. 如何在数据变化时触发相应的副作用函数

在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;
    }
  }
相关推荐
咖啡の猫32 分钟前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲3 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5813 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路4 小时前
GeoTools 读取影像元数据
前端
ssshooter4 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友4 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry5 小时前
Jetpack Compose 中的状态
前端
dae bal6 小时前
关于RSA和AES加密
前端·vue.js
柳杉6 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog6 小时前
低端设备加载webp ANR
前端·算法