6.1 数据绑定概述

Sciter数据绑定机制

Sciter提供了强大的数据绑定功能,允许将JavaScript对象与DOM元素进行双向绑定。这种机制使得数据变化能够自动反映到界面上,同时界面的变化也能自动更新数据模型。

数据绑定特性

  • 双向绑定:数据到视图,视图到数据的自动同步
  • 响应式更新:数据变化时自动更新相关的DOM元素
  • 表达式支持:支持复杂的绑定表达式
  • 条件绑定:基于条件的动态绑定
  • 列表绑定:数组数据的自动渲染
  • 事件绑定:将DOM事件绑定到数据模型方法

绑定语法

<!-- 文本绑定 -->
<span>{user.name}</span>
<div>{user.age} 岁</div>

<!-- 属性绑定 -->
<img src="{user.avatar}" alt="{user.name}" />
<input value="{user.email}" />

<!-- 条件绑定 -->
<div class="{user.isActive ? 'active' : 'inactive'}">
    {user.isActive ? '在线' : '离线'}
</div>

<!-- 循环绑定 -->
<ul>
    <li :each="item in items">{item.name}</li>
</ul>

<!-- 事件绑定 -->
<button onclick="{this.handleClick}">点击</button>

6.2 基础数据绑定

简单数据绑定

1. 文本和属性绑定

// 数据模型
var userModel = {
    name: "张三",
    email: "zhangsan@example.com",
    age: 25,
    avatar: "avatar.jpg",
    isActive: true,
    profile: {
        bio: "软件开发工程师",
        location: "北京",
        website: "https://example.com"
    }
};

// 绑定数据到DOM
function bindUserData() {
    var container = $("#user-profile");
    
    // 设置数据上下文
    container.data = userModel;
    
    // 手动更新绑定(如果需要)
    container.update();
}

// HTML模板
/*
<div id="user-profile">
    <div class="user-header">
        <img src="{avatar}" alt="{name}" class="avatar" />
        <h2>{name}</h2>
        <span class="status {isActive ? 'online' : 'offline'}">
            {isActive ? '在线' : '离线'}
        </span>
    </div>
    
    <div class="user-info">
        <p><strong>邮箱:</strong> {email}</p>
        <p><strong>年龄:</strong> {age} 岁</p>
        <p><strong>简介:</strong> {profile.bio}</p>
        <p><strong>位置:</strong> {profile.location}</p>
        <p><strong>网站:</strong> 
            <a href="{profile.website}" target="_blank">{profile.website}</a>
        </p>
    </div>
</div>
*/

// 更新数据
function updateUserData(newData) {
    Object.assign(userModel, newData);
    
    // 触发更新
    var container = $("#user-profile");
    container.update();
    
    stdout.println("用户数据已更新");
}

// 使用示例
bindUserData();

// 更新用户状态
setTimeout(function() {
    updateUserData({
        isActive: false,
        age: 26
    });
}, 3000);

2. 表单数据绑定

// 表单数据模型
var formModel = {
    user: {
        firstName: "",
        lastName: "",
        email: "",
        phone: "",
        gender: "male",
        birthDate: "",
        country: "CN",
        city: "",
        interests: [],
        newsletter: false,
        terms: false
    },
    
    // 验证规则
    validation: {
        firstName: { required: true, minLength: 2 },
        lastName: { required: true, minLength: 2 },
        email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
        phone: { pattern: /^\d{11}$/ },
        terms: { required: true }
    },
    
    // 错误信息
    errors: {},
    
    // 表单状态
    isSubmitting: false,
    isValid: false
};

// 表单绑定管理器
class FormBinding {
    function this(formElement, model) {
        this.form = formElement;
        this.model = model;
        this.watchers = [];
        
        this.init();
    }
    
    function init() {
        this.bindFormElements();
        this.setupValidation();
        this.setupEvents();
    }
    
    function bindFormElements() {
        var self = this;
        
        // 绑定输入字段
        this.form.$$('input, textarea, select').forEach(function(element) {
            var name = element.name;
            if (!name) return;
            
            // 设置初始值
            self.setElementValue(element, self.getModelValue(name));
            
            // 监听变化
            element.on('input change', function() {
                self.updateModelFromElement(element);
            });
        });
        
        // 绑定复选框组
        this.form.$$('input[type="checkbox"][data-array]').forEach(function(checkbox) {
            var arrayName = checkbox.getAttribute('data-array');
            var value = checkbox.value;
            
            checkbox.on('change', function() {
                self.updateArrayValue(arrayName, value, this.checked);
            });
        });
    }
    
    function setElementValue(element, value) {
        switch (element.type) {
            case 'checkbox':
                element.checked = !!value;
                break;
            case 'radio':
                element.checked = element.value === value;
                break;
            default:
                element.value = value || '';
        }
    }
    
    function getModelValue(path) {
        var parts = path.split('.');
        var value = this.model;
        
        for (var part of parts) {
            value = value && value[part];
        }
        
        return value;
    }
    
    function setModelValue(path, value) {
        var parts = path.split('.');
        var obj = this.model;
        
        for (var i = 0; i < parts.length - 1; i++) {
            if (!obj[parts[i]]) {
                obj[parts[i]] = {};
            }
            obj = obj[parts[i]];
        }
        
        obj[parts[parts.length - 1]] = value;
        
        // 触发验证
        this.validateField(path);
        
        // 通知观察者
        this.notifyWatchers(path, value);
    }
    
    function updateModelFromElement(element) {
        var name = element.name;
        var value;
        
        switch (element.type) {
            case 'checkbox':
                value = element.checked;
                break;
            case 'number':
                value = parseFloat(element.value) || 0;
                break;
            case 'date':
                value = element.value;
                break;
            default:
                value = element.value;
        }
        
        this.setModelValue(name, value);
    }
    
    function updateArrayValue(arrayName, value, checked) {
        var array = this.getModelValue(arrayName) || [];
        var index = array.indexOf(value);
        
        if (checked && index === -1) {
            array.push(value);
        } else if (!checked && index >= 0) {
            array.splice(index, 1);
        }
        
        this.setModelValue(arrayName, array);
    }
    
    function setupValidation() {
        var self = this;
        
        // 实时验证
        this.form.on('input', 'input, textarea, select', function() {
            var name = this.name;
            if (name) {
                self.validateField(name);
            }
        });
        
        // 失去焦点验证
        this.form.on('blur', 'input, textarea, select', function() {
            var name = this.name;
            if (name) {
                self.validateField(name);
                self.updateFieldUI(this, name);
            }
        });
    }
    
    function validateField(fieldName) {
        var value = this.getModelValue(fieldName);
        var rules = this.model.validation[fieldName];
        
        if (!rules) return true;
        
        var errors = [];
        
        // 必填验证
        if (rules.required && (!value || value.toString().trim() === '')) {
            errors.push('此字段为必填项');
        }
        
        // 长度验证
        if (value && rules.minLength && value.toString().length < rules.minLength) {
            errors.push(`最少需要${rules.minLength}个字符`);
        }
        
        if (value && rules.maxLength && value.toString().length > rules.maxLength) {
            errors.push(`最多允许${rules.maxLength}个字符`);
        }
        
        // 模式验证
        if (value && rules.pattern && !rules.pattern.test(value.toString())) {
            errors.push('格式不正确');
        }
        
        // 自定义验证
        if (rules.validator && typeof rules.validator === 'function') {
            var customError = rules.validator(value, this.model);
            if (customError) {
                errors.push(customError);
            }
        }
        
        // 更新错误信息
        if (errors.length > 0) {
            this.model.errors[fieldName] = errors[0];
        } else {
            delete this.model.errors[fieldName];
        }
        
        return errors.length === 0;
    }
    
    function updateFieldUI(element, fieldName) {
        var hasError = this.model.errors[fieldName];
        var container = element.closest('.form-group');
        
        if (hasError) {
            element.addClass('error');
            if (container) {
                container.addClass('has-error');
                
                // 显示错误信息
                var errorElement = container.$('.error-message');
                if (!errorElement) {
                    errorElement = Element.create('div', {
                        class: 'error-message',
                        text: hasError
                    });
                    container.append(errorElement);
                } else {
                    errorElement.innerText = hasError;
                }
            }
        } else {
            element.removeClass('error');
            if (container) {
                container.removeClass('has-error');
                var errorElement = container.$('.error-message');
                if (errorElement) {
                    errorElement.remove();
                }
            }
        }
    }
    
    function setupEvents() {
        var self = this;
        
        // 表单提交
        this.form.on('submit', function(evt) {
            evt.preventDefault();
            self.handleSubmit();
        });
        
        // 重置表单
        this.form.on('reset', function(evt) {
            self.handleReset();
        });
    }
    
    function handleSubmit() {
        // 验证所有字段
        var isValid = this.validateAll();
        
        if (isValid) {
            this.model.isSubmitting = true;
            this.updateSubmitButton();
            
            // 提交数据
            this.submitForm()
                .then(function(result) {
                    stdout.println('表单提交成功');
                    self.handleSubmitSuccess(result);
                })
                .catch(function(error) {
                    stderr.println('表单提交失败: ' + error.message);
                    self.handleSubmitError(error);
                })
                .finally(function() {
                    self.model.isSubmitting = false;
                    self.updateSubmitButton();
                });
        } else {
            stdout.println('表单验证失败');
            this.focusFirstError();
        }
    }
    
    function validateAll() {
        var isValid = true;
        
        for (var fieldName in this.model.validation) {
            if (!this.validateField(fieldName)) {
                isValid = false;
            }
        }
        
        this.model.isValid = isValid;
        return isValid;
    }
    
    function submitForm() {
        return new Promise(function(resolve, reject) {
            // 模拟异步提交
            setTimeout(function() {
                if (Math.random() > 0.1) { // 90% 成功率
                    resolve({ id: Date.now(), message: '提交成功' });
                } else {
                    reject(new Error('服务器错误'));
                }
            }, 2000);
        });
    }
    
    function handleSubmitSuccess(result) {
        // 显示成功消息
        this.showMessage('表单提交成功!', 'success');
        
        // 重置表单(可选)
        // this.handleReset();
    }
    
    function handleSubmitError(error) {
        this.showMessage('提交失败:' + error.message, 'error');
    }
    
    function handleReset() {
        // 重置模型数据
        for (var key in this.model.user) {
            if (typeof this.model.user[key] === 'string') {
                this.model.user[key] = '';
            } else if (typeof this.model.user[key] === 'boolean') {
                this.model.user[key] = false;
            } else if (Array.isArray(this.model.user[key])) {
                this.model.user[key] = [];
            }
        }
        
        // 清除错误
        this.model.errors = {};
        
        // 更新UI
        this.form.$$('input, textarea, select').forEach(function(element) {
            element.removeClass('error');
            var container = element.closest('.form-group');
            if (container) {
                container.removeClass('has-error');
                var errorElement = container.$('.error-message');
                if (errorElement) {
                    errorElement.remove();
                }
            }
        });
        
        stdout.println('表单已重置');
    }
    
    function focusFirstError() {
        for (var fieldName in this.model.errors) {
            var element = this.form.$(`[name="${fieldName}"]`);
            if (element) {
                element.focus();
                break;
            }
        }
    }
    
    function updateSubmitButton() {
        var submitButton = this.form.$('button[type="submit"]');
        if (submitButton) {
            submitButton.disabled = this.model.isSubmitting;
            submitButton.innerText = this.model.isSubmitting ? '提交中...' : '提交';
        }
    }
    
    function showMessage(message, type) {
        var messageElement = Element.create('div', {
            class: `message message-${type}`,
            text: message
        });
        
        this.form.insertBefore(messageElement, this.form.firstChild);
        
        // 自动隐藏
        setTimeout(function() {
            messageElement.remove();
        }, 5000);
    }
    
    // 添加观察者
    function watch(path, callback) {
        this.watchers.push({ path: path, callback: callback });
    }
    
    // 通知观察者
    function notifyWatchers(path, value) {
        for (var watcher of this.watchers) {
            if (watcher.path === path) {
                watcher.callback(value, path);
            }
        }
    }
    
    // 获取表单数据
    function getData() {
        return JSON.parse(JSON.stringify(this.model.user));
    }
    
    // 设置表单数据
    function setData(data) {
        Object.assign(this.model.user, data);
        
        // 更新UI
        this.form.$$('input, textarea, select').forEach(function(element) {
            var name = element.name;
            if (name) {
                self.setElementValue(element, self.getModelValue(name));
            }
        });
    }
}

// 使用示例
function setupFormBinding() {
    var form = $('#user-form');
    var formBinding = new FormBinding(form, formModel);
    
    // 监听字段变化
    formBinding.watch('user.email', function(value) {
        stdout.println('邮箱改变: ' + value);
    });
    
    formBinding.watch('user.firstName', function(value) {
        stdout.println('名字改变: ' + value);
    });
    
    return formBinding;
}

// HTML表单模板
/*
<form id="user-form">
    <div class="form-group">
        <label for="firstName">名字 *</label>
        <input type="text" id="firstName" name="user.firstName" required />
    </div>
    
    <div class="form-group">
        <label for="lastName">姓氏 *</label>
        <input type="text" id="lastName" name="user.lastName" required />
    </div>
    
    <div class="form-group">
        <label for="email">邮箱 *</label>
        <input type="email" id="email" name="user.email" required />
    </div>
    
    <div class="form-group">
        <label for="phone">电话</label>
        <input type="tel" id="phone" name="user.phone" />
    </div>
    
    <div class="form-group">
        <label>性别</label>
        <label><input type="radio" name="user.gender" value="male" checked /> 男</label>
        <label><input type="radio" name="user.gender" value="female" /> 女</label>
    </div>
    
    <div class="form-group">
        <label for="birthDate">出生日期</label>
        <input type="date" id="birthDate" name="user.birthDate" />
    </div>
    
    <div class="form-group">
        <label for="country">国家</label>
        <select id="country" name="user.country">
            <option value="CN">中国</option>
            <option value="US">美国</option>
            <option value="UK">英国</option>
        </select>
    </div>
    
    <div class="form-group">
        <label>兴趣爱好</label>
        <label><input type="checkbox" data-array="user.interests" value="reading" /> 阅读</label>
        <label><input type="checkbox" data-array="user.interests" value="music" /> 音乐</label>
        <label><input type="checkbox" data-array="user.interests" value="sports" /> 运动</label>
        <label><input type="checkbox" data-array="user.interests" value="travel" /> 旅行</label>
    </div>
    
    <div class="form-group">
        <label><input type="checkbox" name="user.newsletter" /> 订阅新闻邮件</label>
    </div>
    
    <div class="form-group">
        <label><input type="checkbox" name="user.terms" required /> 同意服务条款 *</label>
    </div>
    
    <div class="form-actions">
        <button type="submit">提交</button>
        <button type="reset">重置</button>
    </div>
</form>
*/

6.3 高级数据绑定

列表数据绑定

1. 动态列表渲染

// 列表数据模型
var listModel = {
    items: [
        { id: 1, name: "任务1", completed: false, priority: "high", dueDate: "2024-01-15" },
        { id: 2, name: "任务2", completed: true, priority: "medium", dueDate: "2024-01-10" },
        { id: 3, name: "任务3", completed: false, priority: "low", dueDate: "2024-01-20" }
    ],
    filter: "all", // all, active, completed
    sortBy: "name", // name, priority, dueDate
    sortOrder: "asc" // asc, desc
};

// 列表绑定管理器
class ListBinding {
    function this(container, model, itemTemplate) {
        this.container = container;
        this.model = model;
        this.itemTemplate = itemTemplate;
        this.renderedItems = new Map();
        
        this.init();
    }
    
    function init() {
        this.render();
        this.setupEvents();
    }
    
    function render() {
        // 获取过滤和排序后的数据
        var filteredItems = this.getFilteredItems();
        var sortedItems = this.getSortedItems(filteredItems);
        
        // 清空容器
        this.container.innerHTML = '';
        this.renderedItems.clear();
        
        // 渲染项目
        for (var item of sortedItems) {
            var element = this.renderItem(item);
            this.container.append(element);
            this.renderedItems.set(item.id, element);
        }
        
        stdout.println(`渲染了 ${sortedItems.length} 个项目`);
    }
    
    function renderItem(item) {
        // 克隆模板
        var element = this.itemTemplate.cloneNode(true);
        element.removeAttribute('id');
        element.setAttribute('data-item-id', item.id);
        
        // 绑定数据
        this.bindItemData(element, item);
        
        // 设置事件
        this.setupItemEvents(element, item);
        
        return element;
    }
    
    function bindItemData(element, item) {
        // 绑定文本内容
        var nameElement = element.$('.item-name');
        if (nameElement) {
            nameElement.innerText = item.name;
        }
        
        var dueDateElement = element.$('.item-due-date');
        if (dueDateElement) {
            dueDateElement.innerText = this.formatDate(item.dueDate);
        }
        
        // 绑定属性
        var checkbox = element.$('.item-checkbox');
        if (checkbox) {
            checkbox.checked = item.completed;
        }
        
        // 绑定样式类
        element.className = `list-item priority-${item.priority} ${item.completed ? 'completed' : ''}`;
        
        // 绑定优先级
        var priorityElement = element.$('.item-priority');
        if (priorityElement) {
            priorityElement.innerText = this.getPriorityText(item.priority);
            priorityElement.className = `item-priority priority-${item.priority}`;
        }
    }
    
    function setupItemEvents(element, item) {
        var self = this;
        
        // 完成状态切换
        var checkbox = element.$('.item-checkbox');
        if (checkbox) {
            checkbox.on('change', function() {
                self.toggleItemCompleted(item.id, this.checked);
            });
        }
        
        // 编辑项目
        var editButton = element.$('.item-edit');
        if (editButton) {
            editButton.on('click', function() {
                self.editItem(item.id);
            });
        }
        
        // 删除项目
        var deleteButton = element.$('.item-delete');
        if (deleteButton) {
            deleteButton.on('click', function() {
                self.deleteItem(item.id);
            });
        }
        
        // 双击编辑
        element.on('dblclick', function() {
            self.editItem(item.id);
        });
    }
    
    function getFilteredItems() {
        switch (this.model.filter) {
            case 'active':
                return this.model.items.filter(item => !item.completed);
            case 'completed':
                return this.model.items.filter(item => item.completed);
            default:
                return this.model.items;
        }
    }
    
    function getSortedItems(items) {
        var sortBy = this.model.sortBy;
        var sortOrder = this.model.sortOrder;
        
        return items.slice().sort(function(a, b) {
            var aValue = a[sortBy];
            var bValue = b[sortBy];
            
            // 处理不同数据类型
            if (sortBy === 'dueDate') {
                aValue = new Date(aValue);
                bValue = new Date(bValue);
            } else if (sortBy === 'priority') {
                var priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
                aValue = priorityOrder[aValue] || 0;
                bValue = priorityOrder[bValue] || 0;
            }
            
            var result;
            if (aValue < bValue) {
                result = -1;
            } else if (aValue > bValue) {
                result = 1;
            } else {
                result = 0;
            }
            
            return sortOrder === 'desc' ? -result : result;
        });
    }
    
    function addItem(itemData) {
        var newItem = Object.assign({
            id: Date.now(),
            completed: false,
            priority: 'medium',
            dueDate: new Date().toISOString().split('T')[0]
        }, itemData);
        
        this.model.items.push(newItem);
        this.render();
        
        stdout.println('添加项目: ' + newItem.name);
        return newItem;
    }
    
    function updateItem(itemId, updates) {
        var item = this.model.items.find(item => item.id === itemId);
        if (item) {
            Object.assign(item, updates);
            this.render();
            stdout.println('更新项目: ' + item.name);
        }
    }
    
    function deleteItem(itemId) {
        var index = this.model.items.findIndex(item => item.id === itemId);
        if (index >= 0) {
            var item = this.model.items[index];
            this.model.items.splice(index, 1);
            this.render();
            stdout.println('删除项目: ' + item.name);
        }
    }
    
    function toggleItemCompleted(itemId, completed) {
        this.updateItem(itemId, { completed: completed });
    }
    
    function editItem(itemId) {
        var item = this.model.items.find(item => item.id === itemId);
        if (item) {
            this.showEditDialog(item);
        }
    }
    
    function showEditDialog(item) {
        // 创建编辑对话框
        var dialog = Element.create('div', {
            class: 'edit-dialog',
            html: `
                <div class="dialog-content">
                    <h3>编辑任务</h3>
                    <form class="edit-form">
                        <div class="form-group">
                            <label>任务名称:</label>
                            <input type="text" name="name" value="${item.name}" />
                        </div>
                        <div class="form-group">
                            <label>优先级:</label>
                            <select name="priority">
                                <option value="low" ${item.priority === 'low' ? 'selected' : ''}>低</option>
                                <option value="medium" ${item.priority === 'medium' ? 'selected' : ''}>中</option>
                                <option value="high" ${item.priority === 'high' ? 'selected' : ''}>高</option>
                            </select>
                        </div>
                        <div class="form-group">
                            <label>截止日期:</label>
                            <input type="date" name="dueDate" value="${item.dueDate}" />
                        </div>
                        <div class="form-actions">
                            <button type="submit">保存</button>
                            <button type="button" class="cancel">取消</button>
                        </div>
                    </form>
                </div>
            `
        });
        
        document.body.append(dialog);
        
        var self = this;
        var form = dialog.$('.edit-form');
        
        // 处理表单提交
        form.on('submit', function(evt) {
            evt.preventDefault();
            
            var formData = new FormData(this);
            var updates = {
                name: formData.get('name'),
                priority: formData.get('priority'),
                dueDate: formData.get('dueDate')
            };
            
            self.updateItem(item.id, updates);
            dialog.remove();
        });
        
        // 处理取消
        dialog.$('.cancel').on('click', function() {
            dialog.remove();
        });
        
        // 点击外部关闭
        dialog.on('click', function(evt) {
            if (evt.target === this) {
                this.remove();
            }
        });
    }
    
    function setFilter(filter) {
        this.model.filter = filter;
        this.render();
        stdout.println('设置过滤器: ' + filter);
    }
    
    function setSorting(sortBy, sortOrder) {
        this.model.sortBy = sortBy;
        this.model.sortOrder = sortOrder;
        this.render();
        stdout.println(`设置排序: ${sortBy} ${sortOrder}`);
    }
    
    // 辅助方法
    function formatDate(dateString) {
        var date = new Date(dateString);
        return date.toLocaleDateString('zh-CN');
    }
    
    function getPriorityText(priority) {
        switch (priority) {
            case 'high': return '高';
            case 'medium': return '中';
            case 'low': return '低';
            default: return '未知';
        }
    }
    
    // 获取统计信息
    function getStats() {
        var total = this.model.items.length;
        var completed = this.model.items.filter(item => item.completed).length;
        var active = total - completed;
        
        return { total, completed, active };
    }
}

// 使用示例
function setupListBinding() {
    var container = $('#task-list');
    var template = $('#task-item-template');
    
    var listBinding = new ListBinding(container, listModel, template);
    
    // 设置控制按钮
    setupListControls(listBinding);
    
    return listBinding;
}

function setupListControls(listBinding) {
    // 过滤按钮
    $('#filter-all').on('click', function() {
        listBinding.setFilter('all');
        updateFilterButtons('all');
    });
    
    $('#filter-active').on('click', function() {
        listBinding.setFilter('active');
        updateFilterButtons('active');
    });
    
    $('#filter-completed').on('click', function() {
        listBinding.setFilter('completed');
        updateFilterButtons('completed');
    });
    
    // 排序控制
    $('#sort-by').on('change', function() {
        listBinding.setSorting(this.value, listModel.sortOrder);
    });
    
    $('#sort-order').on('change', function() {
        listBinding.setSorting(listModel.sortBy, this.value);
    });
    
    // 添加任务
    $('#add-task-form').on('submit', function(evt) {
        evt.preventDefault();
        
        var formData = new FormData(this);
        var taskData = {
            name: formData.get('name'),
            priority: formData.get('priority') || 'medium',
            dueDate: formData.get('dueDate') || new Date().toISOString().split('T')[0]
        };
        
        listBinding.addItem(taskData);
        this.reset();
    });
    
    // 更新统计信息
    function updateStats() {
        var stats = listBinding.getStats();
        $('#stats-total').innerText = stats.total;
        $('#stats-completed').innerText = stats.completed;
        $('#stats-active').innerText = stats.active;
    }
    
    // 定期更新统计
    setInterval(updateStats, 1000);
    updateStats();
}

function updateFilterButtons(activeFilter) {
    $$('.filter-button').forEach(function(button) {
        button.removeClass('active');
    });
    
    $('#filter-' + activeFilter).addClass('active');
}

// HTML模板
/*
<div class="task-manager">
    <!-- 控制面板 -->
    <div class="controls">
        <form id="add-task-form">
            <input type="text" name="name" placeholder="任务名称" required />
            <select name="priority">
                <option value="low">低优先级</option>
                <option value="medium" selected>中优先级</option>
                <option value="high">高优先级</option>
            </select>
            <input type="date" name="dueDate" />
            <button type="submit">添加</button>
        </form>
        
        <div class="filters">
            <button id="filter-all" class="filter-button active">全部</button>
            <button id="filter-active" class="filter-button">进行中</button>
            <button id="filter-completed" class="filter-button">已完成</button>
        </div>
        
        <div class="sorting">
            <select id="sort-by">
                <option value="name">按名称</option>
                <option value="priority">按优先级</option>
                <option value="dueDate">按日期</option>
            </select>
            <select id="sort-order">
                <option value="asc">升序</option>
                <option value="desc">降序</option>
            </select>
        </div>
        
        <div class="stats">
            <span>总计: <span id="stats-total">0</span></span>
            <span>已完成: <span id="stats-completed">0</span></span>
            <span>进行中: <span id="stats-active">0</span></span>
        </div>
    </div>
    
    <!-- 任务列表 -->
    <div id="task-list" class="task-list"></div>
    
    <!-- 任务项模板 -->
    <div id="task-item-template" class="list-item" style="display: none;">
        <input type="checkbox" class="item-checkbox" />
        <div class="item-content">
            <div class="item-name"></div>
            <div class="item-meta">
                <span class="item-priority"></span>
                <span class="item-due-date"></span>
            </div>
        </div>
        <div class="item-actions">
            <button class="item-edit">编辑</button>
            <button class="item-delete">删除</button>
        </div>
    </div>
</div>
*/

6.4 状态管理

集中式状态管理

1. 状态管理器实现

// 状态管理器
class StateManager {
    function this(initialState = {}) {
        this.state = this.deepClone(initialState);
        this.listeners = [];
        this.middleware = [];
        this.history = [];
        this.historyIndex = -1;
        this.maxHistorySize = 50;
        
        // 保存初始状态
        this.saveToHistory(this.state, 'INIT');
    }
    
    // 获取当前状态
    function getState() {
        return this.deepClone(this.state);
    }
    
    // 获取状态的某个部分
    function getStateSlice(path) {
        var parts = path.split('.');
        var value = this.state;
        
        for (var part of parts) {
            value = value && value[part];
        }
        
        return this.deepClone(value);
    }
    
    // 设置状态
    function setState(newState, actionType = 'SET_STATE') {
        var oldState = this.deepClone(this.state);
        
        // 应用中间件
        var action = {
            type: actionType,
            payload: newState,
            oldState: oldState,
            newState: newState
        };
        
        for (var middleware of this.middleware) {
            action = middleware(action) || action;
        }
        
        // 更新状态
        this.state = this.deepClone(action.newState);
        
        // 保存到历史
        this.saveToHistory(this.state, actionType);
        
        // 通知监听器
        this.notifyListeners(oldState, this.state, actionType);
        
        stdout.println(`状态更新: ${actionType}`);
    }
    
    // 更新状态的某个部分
    function updateState(path, value, actionType = 'UPDATE_STATE') {
        var newState = this.deepClone(this.state);
        this.setNestedValue(newState, path, value);
        this.setState(newState, actionType);
    }
    
    // 合并状态
    function mergeState(updates, actionType = 'MERGE_STATE') {
        var newState = this.deepMerge(this.state, updates);
        this.setState(newState, actionType);
    }
    
    // 添加监听器
    function subscribe(listener, path = null) {
        var subscription = {
            id: Date.now() + Math.random(),
            listener: listener,
            path: path
        };
        
        this.listeners.push(subscription);
        
        // 返回取消订阅函数
        var self = this;
        return function unsubscribe() {
            var index = self.listeners.findIndex(l => l.id === subscription.id);
            if (index >= 0) {
                self.listeners.splice(index, 1);
            }
        };
    }
    
    // 通知监听器
    function notifyListeners(oldState, newState, actionType) {
        for (var subscription of this.listeners) {
            try {
                if (subscription.path) {
                    // 检查指定路径是否发生变化
                    var oldValue = this.getNestedValue(oldState, subscription.path);
                    var newValue = this.getNestedValue(newState, subscription.path);
                    
                    if (!this.deepEqual(oldValue, newValue)) {
                        subscription.listener(newValue, oldValue, actionType);
                    }
                } else {
                    // 监听整个状态
                    subscription.listener(newState, oldState, actionType);
                }
            } catch (error) {
                stderr.println(`监听器错误: ${error.message}`);
            }
        }
    }
    
    // 添加中间件
    function use(middleware) {
        this.middleware.push(middleware);
    }
    
    // 撤销
    function undo() {
        if (this.historyIndex > 0) {
            this.historyIndex--;
            this.state = this.deepClone(this.history[this.historyIndex].state);
            this.notifyListeners({}, this.state, 'UNDO');
            stdout.println('撤销操作');
            return true;
        }
        return false;
    }
    
    // 重做
    function redo() {
        if (this.historyIndex < this.history.length - 1) {
            this.historyIndex++;
            this.state = this.deepClone(this.history[this.historyIndex].state);
            this.notifyListeners({}, this.state, 'REDO');
            stdout.println('重做操作');
            return true;
        }
        return false;
    }
    
    // 保存到历史
    function saveToHistory(state, actionType) {
        // 移除当前位置之后的历史
        this.history = this.history.slice(0, this.historyIndex + 1);
        
        // 添加新状态
        this.history.push({
            state: this.deepClone(state),
            actionType: actionType,
            timestamp: Date.now()
        });
        
        // 限制历史大小
        if (this.history.length > this.maxHistorySize) {
            this.history.shift();
        } else {
            this.historyIndex++;
        }
    }
    
    // 获取历史信息
    function getHistory() {
        return this.history.map(function(entry, index) {
            return {
                index: index,
                actionType: entry.actionType,
                timestamp: entry.timestamp,
                isCurrent: index === this.historyIndex
            };
        }.bind(this));
    }
    
    // 跳转到历史状态
    function jumpToHistory(index) {
        if (index >= 0 && index < this.history.length) {
            this.historyIndex = index;
            this.state = this.deepClone(this.history[index].state);
            this.notifyListeners({}, this.state, 'JUMP_TO_HISTORY');
            stdout.println(`跳转到历史状态: ${index}`);
            return true;
        }
        return false;
    }
    
    // 重置状态
    function reset() {
        if (this.history.length > 0) {
            this.jumpToHistory(0);
        }
    }
    
    // 清除历史
    function clearHistory() {
        this.history = [{
            state: this.deepClone(this.state),
            actionType: 'CLEAR_HISTORY',
            timestamp: Date.now()
        }];
        this.historyIndex = 0;
    }
    
    // 辅助方法
    function deepClone(obj) {
        if (obj === null || typeof obj !== 'object') {
            return obj;
        }
        
        if (obj instanceof Date) {
            return new Date(obj.getTime());
        }
        
        if (obj instanceof Array) {
            return obj.map(item => this.deepClone(item));
        }
        
        var cloned = {};
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                cloned[key] = this.deepClone(obj[key]);
            }
        }
        
        return cloned;
    }
    
    function deepEqual(a, b) {
        if (a === b) return true;
        
        if (a === null || b === null) return false;
        if (typeof a !== typeof b) return false;
        
        if (typeof a === 'object') {
            if (Array.isArray(a) !== Array.isArray(b)) return false;
            
            var keysA = Object.keys(a);
            var keysB = Object.keys(b);
            
            if (keysA.length !== keysB.length) return false;
            
            for (var key of keysA) {
                if (!keysB.includes(key)) return false;
                if (!this.deepEqual(a[key], b[key])) return false;
            }
            
            return true;
        }
        
        return false;
    }
    
    function deepMerge(target, source) {
        var result = this.deepClone(target);
        
        for (var key in source) {
            if (source.hasOwnProperty(key)) {
                if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
                    result[key] = this.deepMerge(result[key] || {}, source[key]);
                } else {
                    result[key] = source[key];
                }
            }
        }
        
        return result;
    }
    
    function getNestedValue(obj, path) {
        var parts = path.split('.');
        var value = obj;
        
        for (var part of parts) {
            value = value && value[part];
        }
        
        return value;
    }
    
    function setNestedValue(obj, path, value) {
        var parts = path.split('.');
        var current = obj;
        
        for (var i = 0; i < parts.length - 1; i++) {
            if (!current[parts[i]]) {
                current[parts[i]] = {};
            }
            current = current[parts[i]];
        }
        
        current[parts[parts.length - 1]] = value;
    }
}

// 创建全局状态管理器
var appState = new StateManager({
    user: {
        id: null,
        name: '',
        email: '',
        isLoggedIn: false,
        preferences: {
            theme: 'light',
            language: 'zh-CN',
            notifications: true
        }
    },
    ui: {
        loading: false,
        sidebarOpen: true,
        currentPage: 'dashboard',
        modal: {
            isOpen: false,
            type: null,
            data: null
        }
    },
    data: {
        tasks: [],
        projects: [],
        notifications: []
    }
});

// 添加日志中间件
appState.use(function(action) {
    stdout.println(`[StateManager] ${action.type}: ${JSON.stringify(action.payload)}`);
    return action;
});

// 添加验证中间件
appState.use(function(action) {
    // 验证用户状态
    if (action.newState.user && action.newState.user.email) {
        var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailPattern.test(action.newState.user.email)) {
            stderr.println('无效的邮箱地址');
            // 可以阻止状态更新
            // return null;
        }
    }
    
    return action;
});

// 状态管理操作
namespace StateActions {
    // 用户操作
    function loginUser(userData) {
        appState.mergeState({
            user: {
                id: userData.id,
                name: userData.name,
                email: userData.email,
                isLoggedIn: true
            }
        }, 'LOGIN_USER');
    }
    
    function logoutUser() {
        appState.updateState('user', {
            id: null,
            name: '',
            email: '',
            isLoggedIn: false
        }, 'LOGOUT_USER');
    }
    
    function updateUserPreferences(preferences) {
        appState.updateState('user.preferences', preferences, 'UPDATE_PREFERENCES');
    }
    
    // UI操作
    function setLoading(loading) {
        appState.updateState('ui.loading', loading, 'SET_LOADING');
    }
    
    function toggleSidebar() {
        var currentState = appState.getStateSlice('ui.sidebarOpen');
        appState.updateState('ui.sidebarOpen', !currentState, 'TOGGLE_SIDEBAR');
    }
    
    function navigateTo(page) {
        appState.updateState('ui.currentPage', page, 'NAVIGATE_TO');
    }
    
    function openModal(type, data) {
        appState.updateState('ui.modal', {
            isOpen: true,
            type: type,
            data: data
        }, 'OPEN_MODAL');
    }
    
    function closeModal() {
        appState.updateState('ui.modal', {
            isOpen: false,
            type: null,
            data: null
        }, 'CLOSE_MODAL');
    }
    
    // 数据操作
    function addTask(task) {
        var tasks = appState.getStateSlice('data.tasks') || [];
        tasks.push(Object.assign({
            id: Date.now(),
            createdAt: new Date().toISOString()
        }, task));
        
        appState.updateState('data.tasks', tasks, 'ADD_TASK');
    }
    
    function updateTask(taskId, updates) {
        var tasks = appState.getStateSlice('data.tasks') || [];
        var taskIndex = tasks.findIndex(task => task.id === taskId);
        
        if (taskIndex >= 0) {
            tasks[taskIndex] = Object.assign(tasks[taskIndex], updates);
            appState.updateState('data.tasks', tasks, 'UPDATE_TASK');
        }
    }
    
    function deleteTask(taskId) {
        var tasks = appState.getStateSlice('data.tasks') || [];
        var filteredTasks = tasks.filter(task => task.id !== taskId);
        appState.updateState('data.tasks', filteredTasks, 'DELETE_TASK');
    }
    
    function addNotification(notification) {
        var notifications = appState.getStateSlice('data.notifications') || [];
        notifications.unshift(Object.assign({
            id: Date.now(),
            timestamp: new Date().toISOString(),
            read: false
        }, notification));
        
        // 限制通知数量
        if (notifications.length > 50) {
            notifications = notifications.slice(0, 50);
        }
        
        appState.updateState('data.notifications', notifications, 'ADD_NOTIFICATION');
    }
    
    // 导出函数
    self.loginUser = loginUser;
    self.logoutUser = logoutUser;
    self.updateUserPreferences = updateUserPreferences;
    self.setLoading = setLoading;
    self.toggleSidebar = toggleSidebar;
    self.navigateTo = navigateTo;
    self.openModal = openModal;
    self.closeModal = closeModal;
    self.addTask = addTask;
    self.updateTask = updateTask;
    self.deleteTask = deleteTask;
    self.addNotification = addNotification;
}

// 状态订阅示例
function setupStateSubscriptions() {
    // 监听用户登录状态
    appState.subscribe(function(isLoggedIn, oldValue) {
        stdout.println(`用户登录状态改变: ${oldValue} -> ${isLoggedIn}`);
        
        if (isLoggedIn) {
            // 用户登录后的操作
            loadUserData();
            showWelcomeMessage();
        } else {
            // 用户登出后的操作
            clearUserData();
            redirectToLogin();
        }
    }, 'user.isLoggedIn');
    
    // 监听主题变化
    appState.subscribe(function(theme) {
        stdout.println(`主题改变: ${theme}`);
        applyTheme(theme);
    }, 'user.preferences.theme');
    
    // 监听加载状态
    appState.subscribe(function(loading) {
        var loadingElement = $('#loading-indicator');
        if (loadingElement) {
            loadingElement.style.display = loading ? 'block' : 'none';
        }
    }, 'ui.loading');
    
    // 监听侧边栏状态
    appState.subscribe(function(sidebarOpen) {
        var sidebar = $('#sidebar');
        var content = $('#main-content');
        
        if (sidebar) {
            sidebar.style.display = sidebarOpen ? 'block' : 'none';
        }
        
        if (content) {
            content.style.marginLeft = sidebarOpen ? '250px' : '0';
        }
    }, 'ui.sidebarOpen');
    
    // 监听模态框状态
    appState.subscribe(function(modal) {
        if (modal.isOpen) {
            showModal(modal.type, modal.data);
        } else {
            hideModal();
        }
    }, 'ui.modal');
    
    // 监听任务列表变化
    appState.subscribe(function(tasks) {
        stdout.println(`任务列表更新: ${tasks.length} 个任务`);
        updateTaskList(tasks);
    }, 'data.tasks');
    
    // 监听通知变化
    appState.subscribe(function(notifications) {
        var unreadCount = notifications.filter(n => !n.read).length;
        updateNotificationBadge(unreadCount);
        
        // 显示最新通知
        if (notifications.length > 0 && !notifications[0].read) {
            showNotificationToast(notifications[0]);
        }
    }, 'data.notifications');
}

// 辅助函数
function loadUserData() {
    StateActions.setLoading(true);
    
    // 模拟异步加载
    setTimeout(function() {
        StateActions.addTask({
            name: '欢迎任务',
            description: '欢迎使用我们的应用!',
            completed: false
        });
        
        StateActions.addNotification({
            type: 'welcome',
            title: '欢迎',
            message: '欢迎回来!'
        });
        
        StateActions.setLoading(false);
    }, 1000);
}

function clearUserData() {
    appState.updateState('data', {
        tasks: [],
        projects: [],
        notifications: []
    }, 'CLEAR_USER_DATA');
}

function applyTheme(theme) {
    document.body.className = document.body.className.replace(/theme-\w+/g, '');
    document.body.addClass('theme-' + theme);
}

function showModal(type, data) {
    var modal = $('#modal');
    if (modal) {
        modal.style.display = 'block';
        // 根据类型显示不同内容
    }
function hideModal() {
    var modal = $('#modal');
    if (modal) {
        modal.style.display = 'none';
    }
}

function updateTaskList(tasks) {
    var taskList = $('#task-list');
    if (taskList) {
        // 重新渲染任务列表
        taskList.innerHTML = '';
        
        for (var task of tasks) {
            var taskElement = Element.create('div', {
                class: 'task-item',
                html: `
                    <input type="checkbox" ${task.completed ? 'checked' : ''} 
                           onchange="StateActions.updateTask(${task.id}, {completed: this.checked})" />
                    <span class="task-name ${task.completed ? 'completed' : ''}">${task.name}</span>
                    <button onclick="StateActions.deleteTask(${task.id})">删除</button>
                `
            });
            taskList.append(taskElement);
        }
    }
}

function updateNotificationBadge(count) {
    var badge = $('#notification-badge');
    if (badge) {
        badge.innerText = count > 0 ? count.toString() : '';
        badge.style.display = count > 0 ? 'inline' : 'none';
    }
}

function showNotificationToast(notification) {
    var toast = Element.create('div', {
        class: 'notification-toast',
        html: `
            <div class="toast-title">${notification.title}</div>
            <div class="toast-message">${notification.message}</div>
        `
    });
    
    document.body.append(toast);
    
    // 自动隐藏
    setTimeout(function() {
        toast.remove();
    }, 3000);
}

function showWelcomeMessage() {
    StateActions.addNotification({
        type: 'info',
        title: '欢迎',
        message: '欢迎使用Sciter应用!'
    });
}

function redirectToLogin() {
    StateActions.navigateTo('login');
}

// 初始化状态管理
function initializeStateManagement() {
    setupStateSubscriptions();
    
    // 模拟用户登录
    setTimeout(function() {
        StateActions.loginUser({
            id: 1,
            name: '张三',
            email: 'zhangsan@example.com'
        });
    }, 1000);
}

// 状态调试工具
namespace StateDebugger {
    function logCurrentState() {
        stdout.println('当前状态:');
        stdout.println(JSON.stringify(appState.getState(), null, 2));
    }
    
    function logHistory() {
        stdout.println('状态历史:');
        var history = appState.getHistory();
        for (var entry of history) {
            stdout.println(`${entry.index}: ${entry.actionType} ${entry.isCurrent ? '(当前)' : ''}`);
        }
    }
    
    function exportState() {
        return JSON.stringify(appState.getState());
    }
    
    function importState(stateJson) {
        try {
            var state = JSON.parse(stateJson);
            appState.setState(state, 'IMPORT_STATE');
            stdout.println('状态导入成功');
        } catch (error) {
            stderr.println('状态导入失败: ' + error.message);
        }
    }
    
    // 导出函数
    self.logCurrentState = logCurrentState;
    self.logHistory = logHistory;
    self.exportState = exportState;
    self.importState = importState;
}

6.5 响应式数据绑定

观察者模式实现

// 响应式数据系统
class ReactiveData {
    function this(data = {}) {
        this.data = {};
        this.observers = new Map();
        this.computedCache = new Map();
        this.computedDeps = new Map();
        
        this.makeReactive(data);
    }
    
    function makeReactive(obj, path = '') {
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                var fullPath = path ? `${path}.${key}` : key;
                
                if (typeof obj[key] === 'object' && obj[key] !== null) {
                    this.data[key] = {};
                    this.makeReactive(obj[key], fullPath);
                } else {
                    this.defineReactiveProperty(obj, key, obj[key], fullPath);
                }
            }
        }
    }
    
    function defineReactiveProperty(obj, key, value, path) {
        var self = this;
        var internalValue = value;
        
        Object.defineProperty(this.data, key, {
            get: function() {
                // 收集依赖
                if (self.currentComputed) {
                    self.addDependency(self.currentComputed, path);
                }
                return internalValue;
            },
            
            set: function(newValue) {
                if (newValue !== internalValue) {
                    var oldValue = internalValue;
                    internalValue = newValue;
                    
                    // 通知观察者
                    self.notify(path, newValue, oldValue);
                    
                    // 更新计算属性
                    self.updateComputedProperties(path);
                }
            },
            
            enumerable: true,
            configurable: true
        });
    }
    
    function watch(path, callback) {
        if (!this.observers.has(path)) {
            this.observers.set(path, []);
        }
        
        this.observers.get(path).push(callback);
        
        // 返回取消监听函数
        var self = this;
        return function unwatch() {
            var callbacks = self.observers.get(path);
            if (callbacks) {
                var index = callbacks.indexOf(callback);
                if (index >= 0) {
                    callbacks.splice(index, 1);
                }
            }
        };
    }
    
    function notify(path, newValue, oldValue) {
        var callbacks = this.observers.get(path);
        if (callbacks) {
            for (var callback of callbacks) {
                try {
                    callback(newValue, oldValue, path);
                } catch (error) {
                    stderr.println(`观察者回调错误: ${error.message}`);
                }
            }
        }
    }
    
    function computed(name, computeFn) {
        var self = this;
        
        // 计算初始值并收集依赖
        this.currentComputed = name;
        var value = computeFn.call(this.data);
        this.currentComputed = null;
        
        this.computedCache.set(name, value);
        
        // 定义计算属性
        Object.defineProperty(this.data, name, {
            get: function() {
                return self.computedCache.get(name);
            },
            enumerable: true,
            configurable: true
        });
        
        return value;
    }
    
    function addDependency(computedName, path) {
        if (!this.computedDeps.has(computedName)) {
            this.computedDeps.set(computedName, new Set());
        }
        
        this.computedDeps.get(computedName).add(path);
    }
    
    function updateComputedProperties(changedPath) {
        for (var [computedName, deps] of this.computedDeps) {
            if (deps.has(changedPath)) {
                this.recomputeProperty(computedName);
            }
        }
    }
    
    function recomputeProperty(name) {
        // 这里需要重新计算,但需要保存原始计算函数
        // 简化实现,实际应用中需要更复杂的依赖追踪
        stdout.println(`重新计算属性: ${name}`);
    }
    
    function get(path) {
        var parts = path.split('.');
        var value = this.data;
        
        for (var part of parts) {
            value = value && value[part];
        }
        
        return value;
    }
    
    function set(path, value) {
        var parts = path.split('.');
        var obj = this.data;
        
        for (var i = 0; i < parts.length - 1; i++) {
            if (!obj[parts[i]]) {
                obj[parts[i]] = {};
            }
            obj = obj[parts[i]];
        }
        
        obj[parts[parts.length - 1]] = value;
    }
}

// 使用示例
var reactiveData = new ReactiveData({
    user: {
        firstName: 'John',
        lastName: 'Doe',
        age: 30
    },
    settings: {
        theme: 'light',
        language: 'en'
    }
});

// 监听数据变化
reactiveData.watch('user.firstName', function(newValue, oldValue) {
    stdout.println(`名字改变: ${oldValue} -> ${newValue}`);
});

// 计算属性
reactiveData.computed('user.fullName', function() {
    return this.user.firstName + ' ' + this.user.lastName;
});

// 测试响应式
reactiveData.set('user.firstName', 'Jane');
stdout.println('全名: ' + reactiveData.get('user.fullName'));

6.6 本章总结

核心要点

  1. 数据绑定机制

    • 双向数据绑定的实现原理
    • 绑定语法和表达式支持
    • 条件绑定和循环绑定
    • 事件绑定和方法调用
  2. 表单数据绑定

    • 表单元素的数据绑定
    • 实时验证和错误处理
    • 复杂表单的状态管理
    • 表单提交和重置
  3. 列表数据绑定

    • 动态列表渲染
    • 列表项的增删改查
    • 过滤和排序功能
    • 列表性能优化
  4. 状态管理

    • 集中式状态管理器
    • 状态订阅和通知机制
    • 中间件和插件系统
    • 状态历史和时间旅行
  5. 响应式数据

    • 观察者模式实现
    • 计算属性和依赖追踪
    • 响应式数据的性能优化
    • 深度监听和批量更新

最佳实践

  1. 数据绑定设计

    • 保持数据模型的简洁性
    • 避免过深的嵌套结构
    • 使用计算属性处理复杂逻辑
    • 合理使用条件绑定
  2. 状态管理策略

    • 按功能模块组织状态
    • 使用不可变数据更新
    • 实现状态的序列化和恢复
    • 添加状态变化的日志记录
  3. 性能优化

    • 避免频繁的DOM更新
    • 使用虚拟滚动处理大列表
    • 实现智能的依赖收集
    • 批量处理状态更新
  4. 调试和测试

    • 提供状态调试工具
    • 实现状态快照功能
    • 编写状态变化的单元测试
    • 监控状态管理的性能

练习题

  1. 基础练习

    • 创建一个用户信息编辑表单,实现双向数据绑定
    • 实现一个待办事项列表,支持添加、删除、完成状态切换
    • 创建一个购物车组件,计算总价和商品数量
  2. 进阶练习

    • 实现一个表格组件,支持排序、过滤、分页功能
    • 创建一个多步骤表单向导,管理各步骤的状态
    • 实现一个聊天应用的消息列表,支持实时更新
  3. 高级练习

    • 设计一个可撤销/重做的绘图应用
    • 实现一个协作编辑器的状态同步机制
    • 创建一个复杂的数据可视化仪表板

下一章预告

下一章我们将学习组件化开发,包括: - 组件的设计和实现 - 组件间的通信机制 - 组件的生命周期管理 - 可复用组件库的构建 - 组件的测试和文档