Vue.js组件生命周期

本章重点:生命周期是Vue组件从创建到销毁的完整过程。理解生命周期钩子函数的使用时机,是掌握Vue组件开发的关键。

什么是生命周期?

组件生命周期是指Vue组件从创建、挂载、更新到销毁的整个过程。在每个阶段,Vue都会调用特定的生命周期钩子函数,允许我们在特定时机执行自定义代码。

初始化

beforeCreate

created

阶段1
挂载

beforeMount

mounted

阶段2
更新

beforeUpdate

updated

阶段3
销毁

beforeDestroy

destroyed

阶段4
创建实例
挂载DOM
响应更新
清理销毁

生命周期钩子时间线

以下是完整的生命周期钩子执行顺序:

1
beforeCreate

实例初始化之后,数据观测和事件配置之前调用

初始化阶段
2
created

实例创建完成,数据观测和事件配置已建立

初始化阶段
3
beforeMount

挂载开始之前调用,模板编译完成,但尚未挂载到DOM

挂载阶段
4
mounted

实例已挂载到DOM,可以访问DOM元素

挂载阶段
5
beforeUpdate

数据更新时调用,DOM尚未重新渲染

更新阶段
6
updated

数据更新后调用,DOM已完成重新渲染

更新阶段
7
beforeDestroy

实例销毁之前调用,此时实例仍然完全可用

销毁阶段
8
destroyed

实例销毁后调用,所有指令已解绑,事件监听器已移除

销毁阶段

生命周期钩子详解

钩子函数 阶段 调用时机 典型用途
beforeCreate 初始化 实例初始化之后,数据观测之前 插件初始化、全局配置
created 初始化 实例创建完成,数据观测已建立 API数据请求、事件监听
beforeMount 挂载 模板编译完成,挂载到DOM之前 最后的初始化工作
mounted 挂载 实例已挂载到DOM DOM操作、第三方库初始化
beforeUpdate 更新 数据更新时,DOM重新渲染之前 获取更新前的DOM状态
updated 更新 数据更新后,DOM已完成重新渲染 依赖DOM的第三方库更新
beforeDestroy 销毁 实例销毁之前 清理定时器、取消事件监听
destroyed 销毁 实例销毁之后 清理引用、通知其他组件

初始化阶段

beforeCreate

在实例初始化之后,数据观测和事件配置之前调用。


export default {
    name: 'LifecycleDemo',

    beforeCreate() {
        console.log('=== beforeCreate ===');
        console.log('数据观测还未建立');
        console.log('$data:', this.$data); // undefined
        console.log('$el:', this.$el);     // undefined
        console.log('methods:', this.myMethod); // undefined

        // 无法访问组件数据和方法
        // console.log(this.message); // 会报错

        // 可以访问组件选项
        console.log('组件名:', this.$options.name);

        // 通常用于插件初始化
        this.initPlugin();
    },

    methods: {
        initPlugin() {
            console.log('初始化插件');
        }
    }
};
                                
created

实例创建完成后调用,此时数据观测和事件配置已完成。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            message: 'Hello Vue!',
            items: [],
            loading: false
        };
    },

    computed: {
        reversedMessage() {
            return this.message.split('').reverse().join('');
        }
    },

    created() {
        console.log('=== created ===');
        console.log('数据观测已建立');
        console.log('$data:', this.$data); // 可以访问
        console.log('message:', this.message); // 'Hello Vue!'
        console.log('computed:', this.reversedMessage); // '!euV olleH'

        // DOM元素仍然不可访问
        console.log('$el:', this.$el); // undefined

        // 最常见的用途:发起API请求
        this.fetchData();

        // 设置事件监听
        window.addEventListener('resize', this.handleResize);

        // 初始化定时器
        this.timer = setInterval(() => {
            console.log('定时器执行');
        }, 1000);
    },

    methods: {
        fetchData() {
            this.loading = true;
            // 模拟API请求
            setTimeout(() => {
                this.items = [
                    { id: 1, name: 'Vue.js' },
                    { id: 2, name: 'React' },
                    { id: 3, name: 'Angular' }
                ];
                this.loading = false;
            }, 1500);
        },

        handleResize() {
            console.log('窗口大小改变');
        }
    }
};
                                

挂载阶段

beforeMount

在挂载开始之前调用,此时模板编译已完成,但尚未挂载到DOM。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            message: '准备挂载'
        };
    },

    beforeMount() {
        console.log('=== beforeMount ===');
        console.log('模板编译已完成');
        console.log('$el:', this.$el); // 仍然指向挂载点元素,不是组件内容

        // 可以访问数据
        console.log('message:', this.message); // '准备挂载'

        // 无法访问渲染后的DOM
        // console.log(document.querySelector('.content')); // null

        // 适合执行挂载前的最后配置
        this.prepareForMount();
    },

    methods: {
        prepareForMount() {
            console.log('准备挂载...');
            // 可以在这里修改数据,但不会触发额外的重新渲染
            this.message = '即将挂载';
        }
    },

    template: `
        <div class="content">
            {{ message }}
        </div>
    `
};
                                
mounted

实例已挂载到DOM,可以访问渲染后的DOM元素。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            message: '已挂载',
            width: 0,
            height: 0
        };
    },

    mounted() {
        console.log('=== mounted ===');
        console.log('实例已挂载到DOM');
        console.log('$el:', this.$el); // 渲染后的DOM元素
        console.log('组件内容:', this.$el.innerHTML);

        // 可以访问DOM元素
        const contentEl = this.$el.querySelector('.content');
        console.log('内容元素:', contentEl);

        // 获取DOM尺寸
        this.width = this.$el.offsetWidth;
        this.height = this.$el.offsetHeight;
        console.log('组件尺寸:', this.width, 'x', this.height);

        // 初始化第三方库(如图表库、地图等)
        this.initChart();
        this.initMap();

        // 添加DOM事件监听
        this.$el.addEventListener('click', this.handleClick);

        // 设置焦点
        if (this.$refs.input) {
            this.$refs.input.focus();
        }
    },

    methods: {
        initChart() {
            // 初始化图表库,如ECharts、Chart.js等
            console.log('初始化图表库');
            // this.chart = echarts.init(this.$el.querySelector('.chart-container'));
        },

        initMap() {
            // 初始化地图库
            console.log('初始化地图');
        },

        handleClick(event) {
            console.log('组件被点击', event.target);
        }
    },

    template: `
        <div class="lifecycle-demo">
            <div class="content">{{ message }}</div>
            <div class="chart-container" ref="chart"></div>
            <input ref="input" type="text" placeholder="获得焦点">
            <p>宽度: {{ width }}px, 高度: {{ height }}px</p>
        </div>
    `
};
                                
最佳实践:
  • mounted钩子中可以进行DOM操作,但应避免直接操作其他组件的DOM
  • 如果组件需要根据DOM尺寸进行布局,应该在mounted中计算
  • 第三方库的DOM相关初始化应该在mounted中进行
  • 如果组件依赖父组件传递的数据,应该在mounted中进行数据验证

更新阶段

beforeUpdate

数据更新时调用,此时DOM尚未重新渲染。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            count: 0,
            previousCount: 0,
            updateLog: []
        };
    },

    beforeUpdate() {
        console.log('=== beforeUpdate ===');
        console.log('数据已更新,DOM尚未重新渲染');
        console.log('count:', this.count);

        // 保存更新前的状态
        this.previousCount = this.count - 1;

        // 获取更新前的DOM信息
        const countElement = this.$el.querySelector('.count-value');
        if (countElement) {
            console.log('DOM中的旧值:', countElement.textContent);
        }

        // 记录更新日志
        this.updateLog.push({
            timestamp: new Date().toISOString(),
            from: this.previousCount,
            to: this.count,
            phase: 'beforeUpdate'
        });

        // 可以在这里取消不必要的更新
        if (this.count > 100) {
            console.warn('计数超过限制,取消更新');
            // 注意:不能在这里直接修改触发更新的数据
            // 否则会导致无限循环
        }
    },

    methods: {
        increment() {
            this.count++;
        },

        decrement() {
            this.count--;
        }
    },

    template: `
        <div>
            <button @click="decrement">-</button>
            <span class="count-value">{{ count }}</span>
            <button @click="increment">+</button>
            <div>更新日志: {{ updateLog.length }} 条</div>
        </div>
    `
};
                                
updated

数据更新后调用,此时DOM已完成重新渲染。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            count: 0,
            width: 0,
            chartData: [],
            chartInstance: null
        };
    },

    updated() {
        console.log('=== updated ===');
        console.log('DOM已重新渲染');
        console.log('count:', this.count);

        // 获取更新后的DOM信息
        const countElement = this.$el.querySelector('.count-value');
        if (countElement) {
            console.log('DOM中的新值:', countElement.textContent);
        }

        // 更新组件尺寸
        this.updateDimensions();

        // 更新图表数据
        this.updateChart();

        // 滚动到最新内容
        this.scrollToBottom();

        // 注意:不要在这里修改触发更新的数据,否则会导致无限循环
        // ❌ 错误示例:this.count = this.count + 1;

        // 可以使用nextTick确保DOM更新完成
        this.$nextTick(() => {
            console.log('DOM更新确认完成');
            this.performPostUpdateOperations();
        });
    },

    methods: {
        updateDimensions() {
            this.width = this.$el.offsetWidth;
            console.log('更新后宽度:', this.width);
        },

        updateChart() {
            if (this.chartInstance) {
                // 更新图表数据
                this.chartInstance.setOption({
                    series: [{
                        data: this.chartData
                    }]
                });
            }
        },

        scrollToBottom() {
            const container = this.$el.querySelector('.log-container');
            if (container) {
                container.scrollTop = container.scrollHeight;
            }
        },

        performPostUpdateOperations() {
            // DOM更新后的操作
            console.log('执行更新后操作');
        }
    }
};
                                
重要警告:
  • 不要在updated钩子中修改触发更新的数据,否则会导致无限循环
  • 使用updated时要格外小心,确保不会引起额外的重新渲染
  • 对于复杂的DOM操作,考虑使用this.$nextTick()确保DOM更新完成
  • 如果只需要响应特定数据的变化,使用watch或计算属性更合适

销毁阶段

beforeDestroy

实例销毁之前调用,此时实例仍然完全可用。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            timer: null,
            eventListeners: [],
            subscriptions: []
        };
    },

    created() {
        // 创建定时器
        this.timer = setInterval(() => {
            console.log('定时器运行中...');
        }, 1000);

        // 添加事件监听
        window.addEventListener('resize', this.handleResize);
        this.eventListeners.push(['resize', this.handleResize]);

        // 创建WebSocket连接
        this.connectWebSocket();
    },

    beforeDestroy() {
        console.log('=== beforeDestroy ===');
        console.log('实例即将销毁,清理资源');

        // 1. 清理定时器
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
            console.log('定时器已清理');
        }

        // 2. 移除事件监听
        this.eventListeners.forEach(([event, handler]) => {
            window.removeEventListener(event, handler);
        });
        this.eventListeners = [];
        console.log('事件监听已移除');

        // 3. 取消API请求
        if (this.requestController) {
            this.requestController.abort();
            console.log('API请求已取消');
        }

        // 4. 断开WebSocket连接
        this.disconnectWebSocket();

        // 5. 清理第三方库实例
        if (this.chartInstance) {
            this.chartInstance.dispose();
            this.chartInstance = null;
            console.log('图表实例已清理');
        }

        // 6. 取消订阅
        this.subscriptions.forEach(subscription => {
            subscription.unsubscribe();
        });
        this.subscriptions = [];

        // 7. 清理DOM引用
        this.$refs = {};

        // 8. 通知父组件或其他组件
        this.$emit('component-will-destroy', this.componentId);

        // 可以访问数据和方法的最后机会
        console.log('最后的数据:', this.$data);
        this.saveState();
    },

    methods: {
        handleResize() {
            console.log('窗口大小改变');
        },

        connectWebSocket() {
            // 模拟WebSocket连接
            console.log('WebSocket连接已建立');
        },

        disconnectWebSocket() {
            console.log('WebSocket连接已断开');
        },

        saveState() {
            // 保存组件状态到localStorage
            localStorage.setItem('component-state', JSON.stringify(this.$data));
        }
    }
};
                                
destroyed

实例销毁后调用,所有指令已解绑,事件监听器已移除。


export default {
    name: 'LifecycleDemo',

    data() {
        return {
            componentId: 'demo-' + Date.now(),
            isDestroyed: false
        };
    },

    destroyed() {
        console.log('=== destroyed ===');
        console.log('实例已完全销毁');

        // 此时组件已不可用
        console.log('$el:', this.$el); // null
        console.log('$data:', this.$data); // 仍然可以访问,但不应该再使用

        // 标记组件已销毁
        this.isDestroyed = true;

        // 清理全局引用
        const globalKey = `component_${this.componentId}`;
        if (window[globalKey]) {
            delete window[globalKey];
        }

        // 通知全局事件总线
        if (this.$root.eventBus) {
            this.$root.eventBus.$emit('component-destroyed', this.componentId);
        }

        // 清理可能的内存泄漏
        this.cleanupMemoryLeaks();

        // 记录销毁日志
        this.logDestruction();

        // 注意:此时不能再调用组件的方法或访问DOM
        // ❌ 错误示例:this.$el.querySelector('...');
        // ❌ 错误示例:this.someMethod();
    },

    methods: {
        cleanupMemoryLeaks() {
            // 清理可能的内存泄漏
            console.log('清理潜在的内存泄漏');

            // 清理闭包中的引用
            this.callbacks = null;
            this.handlers = null;

            // 清理大对象
            this.largeData = null;
            this.buffers = null;
        },

        logDestruction() {
            // 记录销毁信息(可以发送到服务器)
            const logEntry = {
                component: this.$options.name,
                id: this.componentId,
                destroyedAt: new Date().toISOString(),
                duration: Date.now() - this.createdAt
            };

            console.log('销毁日志:', logEntry);

            // 在实际应用中,可以发送到服务器
            // this.$http.post('/api/logs/destruction', logEntry);
        }
    },

    created() {
        this.createdAt = Date.now();
    }
};
                                

完整示例:用户列表组件

一个综合运用生命周期钩子的用户列表组件:


// UserList.vue - 完整的生命周期示例
export default {
    name: 'UserList',

    props: {
        apiUrl: {
            type: String,
            required: true
        },
        autoRefresh: {
            type: Boolean,
            default: true
        },
        pageSize: {
            type: Number,
            default: 10
        }
    },

    data() {
        return {
            users: [],
            loading: false,
            error: null,
            page: 1,
            totalPages: 1,
            refreshTimer: null,
            scrollListener: null,
            resizeObserver: null,
            isOnline: navigator.onLine
        };
    },

    computed: {
        visibleUsers() {
            return this.users.slice(0, this.page * this.pageSize);
        },

        hasMoreUsers() {
            return this.page < this.totalPages;
        }
    },

    // 1. 初始化阶段
    beforeCreate() {
        console.log('UserList: beforeCreate - 组件初始化开始');
        // 此时无法访问数据和方法
    },

    created() {
        console.log('UserList: created - 组件实例已创建');

        // 设置初始状态
        this.loading = true;

        // 发起初始数据请求
        this.fetchUsers();

        // 监听网络状态
        window.addEventListener('online', this.handleOnline);
        window.addEventListener('offline', this.handleOffline);

        // 初始化WebSocket连接(如果需要实时更新)
        this.initWebSocket();

        // 设置自动刷新
        if (this.autoRefresh) {
            this.setupAutoRefresh();
        }
    },

    // 2. 挂载阶段
    beforeMount() {
        console.log('UserList: beforeMount - 即将挂载到DOM');
        // 可以在这里执行挂载前的最后准备
    },

    mounted() {
        console.log('UserList: mounted - 组件已挂载到DOM');

        // 可以访问DOM元素
        this.setupInfiniteScroll();
        this.setupResizeObserver();

        // 初始化第三方库(如虚拟滚动)
        this.initVirtualScroll();

        // 设置焦点
        if (this.$refs.searchInput) {
            this.$refs.searchInput.focus();
        }

        // 记录组件挂载时间(用于性能监控)
        this.mountedAt = Date.now();
        this.reportPerformance();
    },

    // 3. 更新阶段
    beforeUpdate() {
        console.log('UserList: beforeUpdate - 数据即将更新,DOM未渲染');
        // 可以在这里保存更新前的状态
        this.previousUserCount = this.users.length;
    },

    updated() {
        console.log('UserList: updated - 数据已更新,DOM已重新渲染');

        // 更新虚拟滚动
        this.updateVirtualScroll();

        // 滚动到新添加的用户
        this.scrollToNewUsers();

        // 使用nextTick确保DOM更新完成
        this.$nextTick(() => {
            this.performPostUpdateChecks();
        });
    },

    // 4. 销毁阶段
    beforeDestroy() {
        console.log('UserList: beforeDestroy - 组件即将销毁,清理资源');

        // 清理定时器
        if (this.refreshTimer) {
            clearInterval(this.refreshTimer);
            this.refreshTimer = null;
        }

        // 移除事件监听
        window.removeEventListener('online', this.handleOnline);
        window.removeEventListener('offline', this.handleOffline);

        if (this.scrollListener) {
            window.removeEventListener('scroll', this.scrollListener);
        }

        // 断开WebSocket连接
        this.disconnectWebSocket();

        // 清理第三方库
        this.cleanupVirtualScroll();

        if (this.resizeObserver) {
            this.resizeObserver.disconnect();
        }

        // 取消API请求
        if (this.currentRequest) {
            this.currentRequest.abort();
        }

        // 保存组件状态
        this.saveComponentState();
    },

    destroyed() {
        console.log('UserList: destroyed - 组件已销毁');

        // 清理全局引用
        this.cleanupGlobalReferences();

        // 记录销毁日志
        this.logComponentLifetime();
    },

    methods: {
        // 数据获取方法
        async fetchUsers() {
            try {
                this.loading = true;
                this.error = null;

                // 使用AbortController取消请求
                this.currentRequest = new AbortController();

                const response = await fetch(`${this.apiUrl}?page=${this.page}`, {
                    signal: this.currentRequest.signal
                });

                const data = await response.json();
                this.users = data.users;
                this.totalPages = data.totalPages;
                this.page = data.currentPage;

            } catch (error) {
                if (error.name !== 'AbortError') {
                    this.error = error.message;
                    console.error('获取用户数据失败:', error);
                }
            } finally {
                this.loading = false;
                this.currentRequest = null;
            }
        },

        // 自动刷新设置
        setupAutoRefresh() {
            this.refreshTimer = setInterval(() => {
                console.log('自动刷新用户数据');
                this.fetchUsers();
            }, 30000); // 每30秒刷新一次
        },

        // 无限滚动
        setupInfiniteScroll() {
            this.scrollListener = () => {
                const scrollTop = window.scrollY;
                const windowHeight = window.innerHeight;
                const documentHeight = document.documentElement.scrollHeight;

                if (scrollTop + windowHeight >= documentHeight - 100) {
                    this.loadMore();
                }
            };

            window.addEventListener('scroll', this.scrollListener);
        },

        // 虚拟滚动初始化
        initVirtualScroll() {
            // 这里可以初始化虚拟滚动库
            console.log('初始化虚拟滚动');
            // this.virtualScroll = new VirtualScroll(this.$el, {
            //     itemHeight: 50,
            //     renderItem: this.renderUserItem
            // });
        },

        // 网络状态处理
        handleOnline() {
            this.isOnline = true;
            console.log('网络恢复,重新加载数据');
            this.fetchUsers();
        },

        handleOffline() {
            this.isOnline = false;
            console.log('网络断开,显示离线数据');
        },

        // 加载更多
        loadMore() {
            if (this.hasMoreUsers && !this.loading) {
                this.page++;
                this.fetchUsers();
            }
        },

        // 性能报告
        reportPerformance() {
            const loadTime = Date.now() - this.mountedAt;
            console.log(`组件加载时间: ${loadTime}ms`);

            // 在实际应用中,可以发送到性能监控系统
            // this.$gtag('timing', 'component_load', loadTime);
        },

        // 保存组件状态
        saveComponentState() {
            const state = {
                page: this.page,
                users: this.users.slice(0, 20) // 只保存前20个用户
            };

            localStorage.setItem('userList_state', JSON.stringify(state));
        },

        // 清理全局引用
        cleanupGlobalReferences() {
            // 清理可能的内存泄漏
            this.users = null;
            this.callbacks = null;

            // 从全局事件总线移除监听
            if (this.$root.eventBus) {
                this.$root.eventBus.$off('user-updated', this.handleUserUpdated);
            }
        },

        // 记录组件生命周期
        logComponentLifetime() {
            const lifetime = Date.now() - this.mountedAt;
            console.log(`组件存活时间: ${lifetime}ms`);

            // 发送销毁日志到服务器
            const logData = {
                component: 'UserList',
                lifetime: lifetime,
                userCount: this.previousUserCount || 0,
                destroyedAt: new Date().toISOString()
            };

            // 在实际应用中,可以发送到日志系统
            // this.$http.post('/api/logs/component-lifecycle', logData);
        }
    },

    // 模板
    template: `
        <div class="user-list" ref="container">
            <div class="user-list-header">
                <h3>用户列表</h3>
                <input
                    ref="searchInput"
                    type="text"
                    placeholder="搜索用户..."
                    class="search-input"
                >
                <span class="status-badge" :class="isOnline ? 'online' : 'offline'">
                    {{ isOnline ? '在线' : '离线' }}
                </span>
            </div>

            <div v-if="loading && users.length === 0" class="loading">
                加载中...
            </div>

            <div v-else-if="error" class="error">
                {{ error }}
                <button @click="fetchUsers">重试</button>
            </div>

            <div v-else class="user-items">
                <div
                    v-for="user in visibleUsers"
                    :key="user.id"
                    class="user-item"
                >
                    <img :src="user.avatar" :alt="user.name">
                    <div class="user-info">
                        <h4>{{ user.name }}</h4>
                        <p>{{ user.email }}</p>
                    </div>
                </div>
            </div>

            <div v-if="loading && users.length > 0" class="loading-more">
                正在加载更多...
            </div>

            <div v-if="hasMoreUsers && !loading" class="load-more">
                <button @click="loadMore">加载更多</button>
            </div>

            <div class="user-list-footer">
                共 {{ users.length }} 个用户,显示 {{ visibleUsers.length }} 个
            </div>
        </div>
    `
};
                            

生命周期最佳实践

数据请求时机
  • created: 适合初始化数据请求
  • mounted: 适合依赖DOM的数据请求
  • 避免在beforeCreate中请求数据
  • 使用AbortController取消未完成的请求
资源清理
  • 定时器必须在beforeDestroy中清理
  • 事件监听器必须配对移除
  • 第三方库实例需要正确销毁
  • 清理全局引用防止内存泄漏
性能优化
  • 避免在updated中修改触发更新的数据
  • 使用$nextTick确保DOM更新完成
  • 对于高频更新,考虑使用debounce
  • mounted中初始化第三方库

常见陷阱与解决方案

生命周期使用常见问题
无限循环更新

现象:updated中修改数据导致死循环

解决:使用条件判断或计算属性

内存泄漏

现象:组件销毁后仍有引用未清理

解决:beforeDestroy中系统清理

DOM访问过早

现象:created中访问$el

解决:mounted中进行DOM操作

异步操作未清理

现象:异步回调在组件销毁后执行

解决:使用标志位或清理异步操作

生命周期练习

分析以下组件代码,指出生命周期钩子的使用问题:


// 有问题的组件代码
export default {
    name: 'ProblematicComponent',

    data() {
        return {
            timer: null,
            data: [],
            chart: null
        };
    },

    created() {
        // 问题1:DOM操作
        document.querySelector('.some-element').style.color = 'red';

        // 问题2:创建定时器但未清理
        this.timer = setInterval(() => {
            this.fetchData();
        }, 1000);
    },

    mounted() {
        // 问题3:在mounted中重复created中的操作
        this.fetchData();

        // 问题4:初始化图表但未销毁
        this.chart = echarts.init(this.$el);

        // 问题5:添加事件监听但未移除
        window.addEventListener('scroll', () => {
            console.log('滚动');
        });
    },

    updated() {
        // 问题6:在updated中修改触发更新的数据
        if (this.data.length > 100) {
            this.data = this.data.slice(0, 50);
        }

        // 问题7:直接操作DOM
        this.$el.style.backgroundColor = 'yellow';
    },

    methods: {
        fetchData() {
            // 模拟API请求
            setTimeout(() => {
                this.data.push({ id: Date.now() });
            }, 500);
        }
    }

    // 问题8:缺少beforeDestroy/destroyed钩子
};