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;
    }
  }
相关推荐
Mores5 分钟前
开源 | ImageMinify:轻量级智能图片压缩工具,为你的项目瘦身加速
前端
CHQIUU6 分钟前
PDF.js 生态中如何处理“添加注释\添加批注”以及 annotations.contents 属性
开发语言·javascript·pdf
执梦起航7 分钟前
webpack理解与使用
前端·webpack·node.js
ai大师7 分钟前
Cursor怎么使用,3分钟上手Cursor:比ChatGPT更懂需求,用聊天的方式写代码,GPT4、Claude 3.5等先进LLM辅助编程
前端
Json_10 分钟前
使用vue2技术写了一个纯前端的静态网站商城-鲜花销售商城
前端·vue.js·html
1024熙10 分钟前
【Qt】——理解信号与槽,学会使用connect
前端·数据库·c++·qt5
少糖研究所12 分钟前
ColorThief库是如何实现图片取色的?
前端
冴羽12 分钟前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
ZYLAB14 分钟前
我写了一个简易的 SEO 教程,希望能让新手朋友看完以后, SEO 能做到 80 分
前端·seo
小桥风满袖20 分钟前
Three.js-硬要自学系列4 (阵列立方体和相机适配、常见几何体、高光材质、lil-gui库)
前端·css