【前端】Vue2详解:经典的渐进式前端框架

前言

Vue 2是一个渐进式的JavaScript前端框架,以其简洁的API设计、优秀的性能表现和丰富的生态系统而广受开发者喜爱。本文将深入探讨Vue 2的核心概念、特性和最佳实践,帮助开发者全面掌握这个经典的前端框架。

一、Vue 2基础概念

(一)Vue实例

// 创建Vue实例
var vm = new Vue({
    // 挂载点
    el: '#app',
    
    // 数据
    data: {
        message: 'Hello Vue 2!',
        count: 0,
        user: {
            name: 'John',
            age: 30
        }
    },
    
    // 计算属性
    computed: {
        reversedMessage: function() {
            return this.message.split('').reverse().join('');
        },
        
        userInfo: function() {
            return this.user.name + ' (' + this.user.age + '岁)';
        }
    },
    
    // 方法
    methods: {
        increment: function() {
            this.count++;
        },
        
        greet: function() {
            alert('Hello ' + this.user.name + '!');
        }
    },
    
    // 侦听器
    watch: {
        count: function(newVal, oldVal) {
            console.log('count changed from ' + oldVal + ' to ' + newVal);
        }
    }
});
<!-- HTML模板 -->
<div id="app">
    <h1>{{ message }}</h1>
    <p>反转消息: {{ reversedMessage }}</p>
    <p>用户信息: {{ userInfo }}</p>
    
    <div>
        <p>计数: {{ count }}</p>
        <button @click="increment">增加</button>
    </div>
</div>

(二)响应式原理

Vue 2使用Object.defineProperty()实现响应式系统:

// 简化的响应式实现
function defineReactive(obj, key, val) {
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            // 依赖收集
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set: function(newVal) {
            if (newVal === val) return;
            val = newVal;
            // 通知更新
            dep.notify();
        }
    });
}

// 依赖收集器
class Dep {
    constructor() {
        this.subs = [];
    }
    
    addSub(sub) {
        this.subs.push(sub);
    }
    
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}

// 观察者
class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        this.cb = cb;
        this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn);
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        const value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
    
    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

二、Vue 2模板语法

(一)插值表达式

<div id="app">
    <!-- 文本插值 -->
    <p>{{ message }}</p>
    
    <!-- 原始HTML -->
    <div v-html="rawHtml"></div>
    
    <!-- 属性绑定 -->
    <div v-bind:id="dynamicId"></div>
    <div :id="dynamicId"></div> <!-- 简写 -->
    
    <!-- JavaScript表达式 -->
    <p>{{ number + 1 }}</p>
    <p>{{ ok ? 'YES' : 'NO' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>
    
    <!-- 函数调用 -->
    <p>{{ formatDate(new Date()) }}</p>
</div>
new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue 2!',
        rawHtml: '<span style="color: red">红色文本</span>',
        dynamicId: 'my-element',
        number: 10,
        ok: true
    },
    
    methods: {
        formatDate: function(date) {
            return date.toLocaleDateString();
        }
    }
});

(二)指令系统

<div id="directive-app">
    <!-- v-if 条件渲染 -->
    <p v-if="showMessage">这是条件显示的消息</p>
    <p v-else>消息被隐藏了</p>
    
    <!-- v-show 条件显示 -->
    <p v-show="isVisible">这是v-show控制的内容</p>
    
    <!-- v-for 列表渲染 -->
    <ul>
        <li v-for="item in items" :key="item.id">
            {{ item.name }} - {{ item.price }}
        </li>
    </ul>
    
    <!-- v-for 遍历对象 -->
    <div v-for="(value, key) in user" :key="key">
        {{ key }}: {{ value }}
    </div>
    
    <!-- v-on 事件监听 -->
    <button v-on:click="handleClick">点击我</button>
    <button @click="handleClick">点击我 (简写)</button>
    
    <!-- 事件修饰符 -->
    <form @submit.prevent="onSubmit">
        <input @keyup.enter="onEnter" placeholder="按回车键">
        <button type="submit">提交</button>
    </form>
    
    <!-- v-model 双向绑定 -->
    <input v-model="inputValue" placeholder="输入文本">
    <p>输入的内容: {{ inputValue }}</p>
    
    <!-- 复选框 -->
    <input type="checkbox" v-model="checked" id="checkbox">
    <label for="checkbox">{{ checked }}</label>
    
    <!-- 单选按钮 -->
    <input type="radio" id="one" value="One" v-model="picked">
    <label for="one">One</label>
    <input type="radio" id="two" value="Two" v-model="picked">
    <label for="two">Two</label>
    <p>选择的值: {{ picked }}</p>
    
    <!-- 选择框 -->
    <select v-model="selected">
        <option disabled value="">请选择</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <p>选择的选项: {{ selected }}</p>
</div>
new Vue({
    el: '#directive-app',
    data: {
        showMessage: true,
        isVisible: true,
        items: [
            { id: 1, name: '苹果', price: 5 },
            { id: 2, name: '香蕉', price: 3 },
            { id: 3, name: '橙子', price: 4 }
        ],
        user: {
            name: 'John',
            age: 30,
            city: 'New York'
        },
        inputValue: '',
        checked: false,
        picked: '',
        selected: ''
    },
    
    methods: {
        handleClick: function() {
            alert('按钮被点击了!');
        },
        
        onSubmit: function() {
            console.log('表单提交');
        },
        
        onEnter: function() {
            console.log('回车键被按下');
        }
    }
});

三、Vue 2组件系统

(一)组件定义和使用

// 全局组件注册
Vue.component('my-component', {
    props: ['title', 'content'],
    data: function() {
        return {
            count: 0
        };
    },
    template: `
        <div class="my-component">
            <h3>{{ title }}</h3>
            <p>{{ content }}</p>
            <p>点击次数: {{ count }}</p>
            <button @click="count++">点击我</button>
        </div>
    `
});

// 局部组件注册
var ChildComponent = {
    props: ['message'],
    template: '<p>{{ message }}</p>'
};

new Vue({
    el: '#app',
    components: {
        'child-component': ChildComponent
    },
    data: {
        parentMessage: '来自父组件的消息'
    }
});

(二)组件通信

1. 父子组件通信

// 子组件
Vue.component('todo-item', {
    props: ['todo'],
    template: `
        <li>
            {{ todo.text }}
            <button @click="$emit('remove')">删除</button>
        </li>
    `
});

// 父组件
Vue.component('todo-list', {
    data: function() {
        return {
            todos: [
                { id: 1, text: '学习Vue' },
                { id: 2, text: '写代码' },
                { id: 3, text: '睡觉' }
            ]
        };
    },
    methods: {
        removeTodo: function(index) {
            this.todos.splice(index, 1);
        }
    },
    template: `
        <ul>
            <todo-item
                v-for="(todo, index) in todos"
                :key="todo.id"
                :todo="todo"
                @remove="removeTodo(index)"
            ></todo-item>
        </ul>
    `
});

2. 兄弟组件通信(事件总线)

// 创建事件总线
var EventBus = new Vue();

// 组件A
Vue.component('component-a', {
    data: function() {
        return {
            message: ''
        };
    },
    methods: {
        sendMessage: function() {
            EventBus.$emit('message-sent', this.message);
            this.message = '';
        }
    },
    template: `
        <div>
            <h3>组件A</h3>
            <input v-model="message" placeholder="输入消息">
            <button @click="sendMessage">发送消息</button>
        </div>
    `
});

// 组件B
Vue.component('component-b', {
    data: function() {
        return {
            receivedMessage: ''
        };
    },
    mounted: function() {
        var self = this;
        EventBus.$on('message-sent', function(message) {
            self.receivedMessage = message;
        });
    },
    beforeDestroy: function() {
        EventBus.$off('message-sent');
    },
    template: `
        <div>
            <h3>组件B</h3>
            <p>接收到的消息: {{ receivedMessage }}</p>
        </div>
    `
});

(三)插槽系统

// 基础插槽
Vue.component('alert-box', {
    template: `
        <div class="demo-alert-box">
            <strong>Error!</strong>
            <slot></slot>
        </div>
    `
});

// 具名插槽
Vue.component('base-layout', {
    template: `
        <div class="container">
            <header>
                <slot name="header"></slot>
            </header>
            <main>
                <slot></slot>
            </main>
            <footer>
                <slot name="footer"></slot>
            </footer>
        </div>
    `
});

// 作用域插槽
Vue.component('todo-list', {
    data: function() {
        return {
            todos: [
                { id: 1, text: '学习Vue', isComplete: false },
                { id: 2, text: '写代码', isComplete: true }
            ]
        };
    },
    template: `
        <ul>
            <li v-for="todo in todos" :key="todo.id">
                <slot :todo="todo">
                    {{ todo.text }}
                </slot>
            </li>
        </ul>
    `
});
<!-- 使用插槽 -->
<div id="app">
    <!-- 基础插槽 -->
    <alert-box>
        Something bad happened.
    </alert-box>
    
    <!-- 具名插槽 -->
    <base-layout>
        <template v-slot:header>
            <h1>Here might be a page title</h1>
        </template>
        
        <p>A paragraph for the main content.</p>
        <p>And another one.</p>
        
        <template v-slot:footer>
            <p>Here's some contact info</p>
        </template>
    </base-layout>
    
    <!-- 作用域插槽 -->
    <todo-list>
        <template v-slot:default="slotProps">
            <span :class="{ completed: slotProps.todo.isComplete }">
                {{ slotProps.todo.text }}
            </span>
        </template>
    </todo-list>
</div>

四、Vue 2生命周期

(一)生命周期钩子

new Vue({
    el: '#lifecycle-app',
    data: {
        message: 'Hello Vue 2 Lifecycle!',
        items: []
    },
    
    // 创建前
    beforeCreate: function() {
        console.log('beforeCreate: 实例初始化之后,数据观测和事件配置之前');
        console.log('data:', this.message); // undefined
    },
    
    // 创建后
    created: function() {
        console.log('created: 实例创建完成,数据观测、属性和方法的运算已完成');
        console.log('data:', this.message); // Hello Vue 2 Lifecycle!
        
        // 可以进行数据请求
        this.fetchData();
    },
    
    // 挂载前
    beforeMount: function() {
        console.log('beforeMount: 挂载开始之前,render函数首次被调用');
        console.log('$el:', this.$el); // undefined
    },
    
    // 挂载后
    mounted: function() {
        console.log('mounted: 实例挂载到DOM上');
        console.log('$el:', this.$el); // DOM元素
        
        // 可以进行DOM操作
        this.initializeComponents();
    },
    
    // 更新前
    beforeUpdate: function() {
        console.log('beforeUpdate: 数据更新时调用,发生在虚拟DOM重新渲染之前');
    },
    
    // 更新后
    updated: function() {
        console.log('updated: 数据更新导致的虚拟DOM重新渲染后调用');
    },
    
    // 销毁前
    beforeDestroy: function() {
        console.log('beforeDestroy: 实例销毁之前调用');
        
        // 清理工作
        this.cleanup();
    },
    
    // 销毁后
    destroyed: function() {
        console.log('destroyed: 实例销毁后调用');
    },
    
    methods: {
        fetchData: function() {
            // 模拟数据请求
            setTimeout(() => {
                this.items = ['Item 1', 'Item 2', 'Item 3'];
            }, 1000);
        },
        
        initializeComponents: function() {
            // 初始化第三方组件
            console.log('初始化第三方组件');
        },
        
        cleanup: function() {
            // 清理定时器、事件监听器等
            console.log('清理资源');
        }
    }
});

(二)生命周期应用场景

Vue.component('data-table', {
    data: function() {
        return {
            data: [],
            loading: false,
            timer: null
        };
    },
    
    created: function() {
        // 组件创建时获取数据
        this.loadData();
    },
    
    mounted: function() {
        // 挂载后设置定时刷新
        this.timer = setInterval(() => {
            this.loadData();
        }, 30000); // 30秒刷新一次
        
        // 设置事件监听器
        window.addEventListener('resize', this.handleResize);
    },
    
    beforeDestroy: function() {
        // 销毁前清理定时器和事件监听器
        if (this.timer) {
            clearInterval(this.timer);
        }
        window.removeEventListener('resize', this.handleResize);
    },
    
    methods: {
        loadData: function() {
            this.loading = true;
            
            // 模拟API请求
            fetch('/api/data')
                .then(response => response.json())
                .then(data => {
                    this.data = data;
                    this.loading = false;
                })
                .catch(error => {
                    console.error('数据加载失败:', error);
                    this.loading = false;
                });
        },
        
        handleResize: function() {
            // 处理窗口大小变化
            console.log('窗口大小发生变化');
        }
    },
    
    template: `
        <div class="data-table">
            <div v-if="loading">加载中...</div>
            <table v-else>
                <tr v-for="item in data" :key="item.id">
                    <td>{{ item.name }}</td>
                    <td>{{ item.value }}</td>
                </tr>
            </table>
        </div>
    `
});

五、Vue 2状态管理(Vuex)

(一)Vuex基础

// 安装Vuex
// npm install vuex@3

// store/index.js
const store = new Vuex.Store({
    // 状态
    state: {
        count: 0,
        user: null,
        todos: []
    },
    
    // 获取器(计算属性)
    getters: {
        doneTodos: function(state) {
            return state.todos.filter(function(todo) {
                return todo.done;
            });
        },
        
        doneTodosCount: function(state, getters) {
            return getters.doneTodos.length;
        },
        
        getTodoById: function(state) {
            return function(id) {
                return state.todos.find(function(todo) {
                    return todo.id === id;
                });
            };
        }
    },
    
    // 同步修改状态
    mutations: {
        INCREMENT: function(state) {
            state.count++;
        },
        
        SET_USER: function(state, user) {
            state.user = user;
        },
        
        ADD_TODO: function(state, todo) {
            state.todos.push(todo);
        },
        
        REMOVE_TODO: function(state, todoId) {
            const index = state.todos.findIndex(function(todo) {
                return todo.id === todoId;
            });
            if (index > -1) {
                state.todos.splice(index, 1);
            }
        },
        
        TOGGLE_TODO: function(state, todoId) {
            const todo = state.todos.find(function(todo) {
                return todo.id === todoId;
            });
            if (todo) {
                todo.done = !todo.done;
            }
        }
    },
    
    // 异步操作
    actions: {
        increment: function(context) {
            context.commit('INCREMENT');
        },
        
        fetchUser: function(context, userId) {
            return fetch('/api/users/' + userId)
                .then(function(response) {
                    return response.json();
                })
                .then(function(user) {
                    context.commit('SET_USER', user);
                    return user;
                });
        },
        
        addTodo: function(context, todoText) {
            const todo = {
                id: Date.now(),
                text: todoText,
                done: false
            };
            context.commit('ADD_TODO', todo);
        },
        
        removeTodo: function(context, todoId) {
            context.commit('REMOVE_TODO', todoId);
        },
        
        toggleTodo: function(context, todoId) {
            context.commit('TOGGLE_TODO', todoId);
        }
    }
});

// 在Vue实例中使用
new Vue({
    el: '#app',
    store: store,
    // ...
});

(二)Vuex模块化

// store/modules/user.js
const userModule = {
    namespaced: true,
    
    state: {
        profile: null,
        preferences: {}
    },
    
    getters: {
        fullName: function(state) {
            return state.profile ? state.profile.firstName + ' ' + state.profile.lastName : '';
        },
        
        isLoggedIn: function(state) {
            return !!state.profile;
        }
    },
    
    mutations: {
        SET_PROFILE: function(state, profile) {
            state.profile = profile;
        },
        
        UPDATE_PREFERENCES: function(state, preferences) {
            state.preferences = Object.assign({}, state.preferences, preferences);
        },
        
        CLEAR_USER: function(state) {
            state.profile = null;
            state.preferences = {};
        }
    },
    
    actions: {
        login: function(context, credentials) {
            return fetch('/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(credentials)
            })
            .then(function(response) {
                return response.json();
            })
            .then(function(data) {
                context.commit('SET_PROFILE', data.user);
                localStorage.setItem('token', data.token);
                return data;
            });
        },
        
        logout: function(context) {
            context.commit('CLEAR_USER');
            localStorage.removeItem('token');
        },
        
        updatePreferences: function(context, preferences) {
            context.commit('UPDATE_PREFERENCES', preferences);
            
            // 同步到服务器
            return fetch('/api/user/preferences', {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + localStorage.getItem('token')
                },
                body: JSON.stringify(preferences)
            });
        }
    }
};

// store/modules/products.js
const productsModule = {
    namespaced: true,
    
    state: {
        items: [],
        loading: false
    },
    
    getters: {
        availableProducts: function(state) {
            return state.items.filter(function(product) {
                return product.inventory > 0;
            });
        }
    },
    
    mutations: {
        SET_PRODUCTS: function(state, products) {
            state.items = products;
        },
        
        SET_LOADING: function(state, loading) {
            state.loading = loading;
        },
        
        DECREMENT_PRODUCT_INVENTORY: function(state, productId) {
            const product = state.items.find(function(p) {
                return p.id === productId;
            });
            if (product) {
                product.inventory--;
            }
        }
    },
    
    actions: {
        fetchProducts: function(context) {
            context.commit('SET_LOADING', true);
            
            return fetch('/api/products')
                .then(function(response) {
                    return response.json();
                })
                .then(function(products) {
                    context.commit('SET_PRODUCTS', products);
                    context.commit('SET_LOADING', false);
                })
                .catch(function(error) {
                    console.error('获取产品失败:', error);
                    context.commit('SET_LOADING', false);
                });
        }
    }
};

// store/index.js - 组合模块
const store = new Vuex.Store({
    modules: {
        user: userModule,
        products: productsModule
    }
});

(三)在组件中使用Vuex

// 使用mapState、mapGetters、mapMutations、mapActions
Vue.component('user-dashboard', {
    computed: {
        // 映射state
        ...Vuex.mapState({
            count: function(state) {
                return state.count;
            },
            todos: 'todos'
        }),
        
        // 映射getters
        ...Vuex.mapGetters([
            'doneTodos',
            'doneTodosCount'
        ]),
        
        // 映射模块化的state和getters
        ...Vuex.mapState('user', {
            userProfile: 'profile'
        }),
        
        ...Vuex.mapGetters('user', [
            'fullName',
            'isLoggedIn'
        ])
    },
    
    methods: {
        // 映射mutations
        ...Vuex.mapMutations([
            'INCREMENT',
            'ADD_TODO'
        ]),
        
        // 映射actions
        ...Vuex.mapActions([
            'fetchUser',
            'addTodo'
        ]),
        
        // 映射模块化的actions
        ...Vuex.mapActions('user', [
            'login',
            'logout'
        ]),
        
        // 自定义方法
        handleAddTodo: function() {
            if (this.newTodoText.trim()) {
                this.addTodo(this.newTodoText);
                this.newTodoText = '';
            }
        },
        
        handleLogin: function() {
            this.login({
                username: this.username,
                password: this.password
            })
            .then(function() {
                console.log('登录成功');
            })
            .catch(function(error) {
                console.error('登录失败:', error);
            });
        }
    },
    
    data: function() {
        return {
            newTodoText: '',
            username: '',
            password: ''
        };
    },
    
    template: `
        <div class="user-dashboard">
            <div v-if="isLoggedIn">
                <h2>欢迎, {{ fullName }}!</h2>
                <p>计数: {{ count }}</p>
                <button @click="INCREMENT">增加</button>
                
                <div class="todo-section">
                    <h3>待办事项 ({{ doneTodosCount }}/{{ todos.length }})</h3>
                    <input v-model="newTodoText" @keyup.enter="handleAddTodo" placeholder="添加新任务">
                    <button @click="handleAddTodo">添加</button>
                    
                    <ul>
                        <li v-for="todo in todos" :key="todo.id">
                            {{ todo.text }} - {{ todo.done ? '已完成' : '未完成' }}
                        </li>
                    </ul>
                </div>
                
                <button @click="logout">退出登录</button>
            </div>
            
            <div v-else class="login-form">
                <h2>请登录</h2>
                <input v-model="username" placeholder="用户名">
                <input v-model="password" type="password" placeholder="密码">
                <button @click="handleLogin">登录</button>
            </div>
        </div>
    `
});

六、Vue 2路由(Vue Router)

(一)基础路由配置

// 安装Vue Router
// npm install vue-router@3

// 1. 定义路由组件
const Home = { template: '<div>Home</div>' };
const About = { template: '<div>About</div>' };
const User = {
    template: '<div>User {{ $route.params.id }}</div>'
};

// 2. 定义路由
const routes = [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/user/:id', component: User },
    
    // 动态路由匹配
    { path: '/user/:id', component: User },
    { path: '/user/:id/profile', component: UserProfile },
    { path: '/user/:id/posts', component: UserPosts },
    
    // 嵌套路由
    {
        path: '/user/:id',
        component: User,
        children: [
            // 空路径表示默认子路由
            { path: '', component: UserHome },
            { path: 'profile', component: UserProfile },
            { path: 'posts', component: UserPosts }
        ]
    },
    
    // 命名路由
    {
        path: '/user/:userId',
        name: 'user',
        component: User
    },
    
    // 重定向
    { path: '/home', redirect: '/' },
    { path: '/user', redirect: to => {
        // 动态重定向
        return '/user/123';
    }},
    
    // 别名
    { path: '/', component: Home, alias: '/home' },
    
    // 404页面
    { path: '*', component: NotFound }
];

// 3. 创建router实例
const router = new VueRouter({
    mode: 'history', // 使用HTML5 History模式
    base: '/app/', // 应用的基路径
    routes: routes,
    
    // 滚动行为
    scrollBehavior: function(to, from, savedPosition) {
        if (savedPosition) {
            return savedPosition;
        } else {
            return { x: 0, y: 0 };
        }
    }
});

// 4. 创建和挂载根实例
const app = new Vue({
    router: router
}).$mount('#app');

(二)路由守卫

// 全局前置守卫
router.beforeEach(function(to, from, next) {
    console.log('导航到:', to.path);
    
    // 检查是否需要登录
    if (to.matched.some(function(record) {
        return record.meta.requiresAuth;
    })) {
        // 检查用户是否已登录
        if (!isLoggedIn()) {
            next({
                path: '/login',
                query: { redirect: to.fullPath }
            });
        } else {
            next();
        }
    } else {
        next(); // 确保一定要调用 next()
    }
});

// 全局后置钩子
router.afterEach(function(to, from) {
    console.log('导航完成:', to.path);
    // 更新页面标题
    document.title = to.meta.title || 'My App';
});

// 路由独享的守卫
const routes = [
    {
        path: '/admin',
        component: Admin,
        beforeEnter: function(to, from, next) {
            // 检查管理员权限
            if (isAdmin()) {
                next();
            } else {
                next('/unauthorized');
            }
        }
    }
];

// 组件内的守卫
const UserProfile = {
    template: '...',
    
    beforeRouteEnter: function(to, from, next) {
        // 在渲染该组件的对应路由被 confirm 前调用
        // 不能获取组件实例 `this`
        // 因为当守卫执行前,组件实例还没被创建
        getUserProfile(to.params.id, function(err, user) {
            if (err) {
                next(false);
            } else {
                next(function(vm) {
                    // 通过 `vm` 访问组件实例
                    vm.user = user;
                });
            }
        });
    },
    
    beforeRouteUpdate: function(to, from, next) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
        // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        // 可以访问组件实例 `this`
        this.user = getUserProfile(to.params.id);
        next();
    },
    
    beforeRouteLeave: function(to, from, next) {
        // 导航离开该组件的对应路由时调用
        // 可以访问组件实例 `this`
        if (this.hasUnsavedChanges) {
            const answer = window.confirm('你有未保存的更改,确定要离开吗?');
            if (answer) {
                next();
            } else {
                next(false);
            }
        } else {
            next();
        }
    }
};

function isLoggedIn() {
    return !!localStorage.getItem('token');
}

function isAdmin() {
    const user = JSON.parse(localStorage.getItem('user') || '{}');
    return user.role === 'admin';
}

(三)在组件中使用路由

// 编程式导航
Vue.component('navigation-example', {
    methods: {
        goToUser: function(userId) {
            // 字符串
            this.$router.push('/user/' + userId);
            
            // 对象
            this.$router.push({ path: '/user/' + userId });
            
            // 命名的路由
            this.$router.push({ name: 'user', params: { userId: userId } });
            
            // 带查询参数,变成 /register?plan=private
            this.$router.push({ path: '/register', query: { plan: 'private' } });
        },
        
        goBack: function() {
            this.$router.go(-1);
        },
        
        replaceRoute: function() {
            // 替换当前路由,不会向 history 添加新记录
            this.$router.replace('/new-path');
        }
    },
    
    template: `
        <div>
            <button @click="goToUser(123)">Go to User 123</button>
            <button @click="goBack">Go Back</button>
            <button @click="replaceRoute">Replace Route</button>
        </div>
    `
});

// 响应路由参数的变化
Vue.component('user-profile', {
    data: function() {
        return {
            user: null
        };
    },
    
    created: function() {
        this.fetchUser();
    },
    
    watch: {
        // 如果路由有变化,会再次执行该方法
        '$route': function(to, from) {
            this.fetchUser();
        }
    },
    
    methods: {
        fetchUser: function() {
            const userId = this.$route.params.id;
            // 获取用户数据
            fetch('/api/users/' + userId)
                .then(function(response) {
                    return response.json();
                })
                .then(function(user) {
                    this.user = user;
                }.bind(this));
        }
    },
    
    template: `
        <div v-if="user">
            <h2>{{ user.name }}</h2>
            <p>{{ user.email }}</p>
        </div>
    `
});

// 使用路由信息
Vue.component('route-info', {
    computed: {
        routeInfo: function() {
            return {
                path: this.$route.path,
                params: this.$route.params,
                query: this.$route.query,
                hash: this.$route.hash,
                fullPath: this.$route.fullPath,
                matched: this.$route.matched,
                name: this.$route.name
            };
        }
    },
    
    template: `
        <div>
            <h3>当前路由信息:</h3>
            <pre>{{ JSON.stringify(routeInfo, null, 2) }}</pre>
        </div>
    `
});

七、Vue 2响应式原理深入

(一)响应式系统原理

// 简化版的Vue 2响应式系统实现

// 依赖收集器
class Dep {
    constructor() {
        this.subs = [];
    }
    
    addSub(sub) {
        this.subs.push(sub);
    }
    
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    
    notify() {
        this.subs.forEach(sub => sub.update());
    }
}

Dep.target = null;

// 观察者
class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        this.cb = cb;
        this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn);
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        const value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
    
    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

// 响应式处理
function defineReactive(obj, key, val) {
    const dep = new Dep();
    
    // 递归处理嵌套对象
    if (typeof val === 'object' && val !== null) {
        observe(val);
    }
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            // 依赖收集
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set: function(newVal) {
            if (newVal === val) return;
            
            // 新值也需要观察
            if (typeof newVal === 'object' && newVal !== null) {
                observe(newVal);
            }
            
            val = newVal;
            // 通知更新
            dep.notify();
        }
    });
}

function observe(value) {
    if (typeof value !== 'object' || value === null) {
        return;
    }
    
    Object.keys(value).forEach(key => {
        defineReactive(value, key, value[key]);
    });
}

// 解析路径
function parsePath(path) {
    const segments = path.split('.');
    return function(obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]];
        }
        return obj;
    };
}

// 使用示例
const data = {
    message: 'Hello Vue!',
    count: 0,
    user: {
        name: 'John',
        age: 30
    }
};

// 使数据响应式
Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
});

// 创建观察者
new Watcher(data, 'message', (newVal, oldVal) => {
    console.log(`message changed: ${oldVal} -> ${newVal}`);
});

new Watcher(data, 'count', (newVal, oldVal) => {
    console.log(`count changed: ${oldVal} -> ${newVal}`);
});

// 测试响应式
data.message = 'Hello World!'; // 触发更新
data.count = 1; // 触发更新

(二)数组响应式处理

// Vue 2中数组响应式的实现
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

// 需要拦截的数组方法
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

methodsToPatch.forEach(function(method) {
    // 缓存原始方法
    const original = arrayProto[method];
    
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 调用原始方法
            const result = original.apply(this, args);
            
            // 获取观察者
            const ob = this.__ob__;
            let inserted;
            
            switch (method) {
                case 'push':
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice':
                    inserted = args.slice(2);
                    break;
            }
            
            // 对新插入的元素进行观察
            if (inserted) {
                ob.observeArray(inserted);
            }
            
            // 通知更新
            ob.dep.notify();
            
            return result;
        },
        enumerable: false,
        writable: true,
        configurable: true
    });
});

// 观察者类
class Observer {
    constructor(value) {
        this.value = value;
        this.dep = new Dep();
        
        // 在对象上定义__ob__属性
        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,
            writable: true,
            configurable: true
        });
        
        if (Array.isArray(value)) {
            // 数组处理
            value.__proto__ = arrayMethods;
            this.observeArray(value);
        } else {
            // 对象处理
            this.walk(value);
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    }
    
    observeArray(items) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    }
}

function observe(value) {
    if (typeof value !== 'object' || value === null) {
        return;
    }
    
    let ob;
    if (value.__ob__ && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    
    return ob;
}

// 使用示例
const vm = new Vue({
    data: {
        items: ['apple', 'banana', 'orange'],
        users: [
            { name: 'Alice', age: 25 },
            { name: 'Bob', age: 30 }
        ]
    },
    
    watch: {
        items: {
            handler: function(newItems, oldItems) {
                console.log('items数组发生变化:', newItems);
            },
            deep: true
        },
        
        users: {
            handler: function(newUsers, oldUsers) {
                console.log('users数组发生变化:', newUsers);
            },
            deep: true
        }
    },
    
    methods: {
        addItem: function() {
            this.items.push('grape'); // 会触发更新
        },
        
        removeItem: function() {
            this.items.pop(); // 会触发更新
        },
        
        updateUser: function() {
            // 直接修改数组索引不会触发更新(Vue 2的限制)
            // this.users[0] = { name: 'Charlie', age: 35 }; // 不会触发更新
            
            // 正确的方式
            this.$set(this.users, 0, { name: 'Charlie', age: 35 });
            // 或者
            // this.users.splice(0, 1, { name: 'Charlie', age: 35 });
        }
    }
});

八、Vue 2性能优化

(一)组件优化

// 1. 使用Object.freeze()冻结数据
Vue.component('large-list', {
    data: function() {
        return {
            // 冻结大型数据,避免响应式处理
            staticData: Object.freeze([
                { id: 1, name: 'Item 1', value: 100 },
                { id: 2, name: 'Item 2', value: 200 },
                // ... 大量数据
            ]),
            
            // 需要响应式的数据
            selectedId: null,
            loading: false
        };
    },
    
    template: `
        <div class="large-list">
            <div 
                v-for="item in staticData" 
                :key="item.id"
                :class="{ selected: item.id === selectedId }"
                @click="selectedId = item.id"
            >
                {{ item.name }}: {{ item.value }}
            </div>
        </div>
    `
});

// 2. 使用$once监听一次性事件
Vue.component('expensive-component', {
    mounted: function() {
        // 只监听一次resize事件
        this.$once('hook:beforeDestroy', () => {
            window.removeEventListener('resize', this.handleResize);
        });
        
        window.addEventListener('resize', this.handleResize);
    },
    
    methods: {
        handleResize: function() {
            // 处理窗口大小变化
        }
    }
});

// 3. 使用$nextTick优化DOM操作
Vue.component('dom-optimizer', {
    data: function() {
        return {
            items: []
        };
    },
    
    methods: {
        addItems: function() {
            // 批量添加数据
            for (let i = 0; i < 1000; i++) {
                this.items.push({ id: i, name: `Item ${i}` });
            }
            
            // 等待DOM更新完成后执行
            this.$nextTick(() => {
                // 滚动到底部
                const container = this.$refs.container;
                container.scrollTop = container.scrollHeight;
            });
        }
    },
    
    template: `
        <div>
            <button @click="addItems">添加项目</button>
            <div ref="container" class="item-container">
                <div v-for="item in items" :key="item.id">
                    {{ item.name }}
                </div>
            </div>
        </div>
    `
});

// 4. 防抖和节流
Vue.component('search-input', {
    data: function() {
        return {
            searchQuery: '',
            searchResults: []
        };
    },
    
    created: function() {
        // 创建防抖函数
        this.debouncedSearch = this.debounce(this.performSearch, 300);
    },
    
    watch: {
        searchQuery: function() {
            this.debouncedSearch();
        }
    },
    
    methods: {
        // 防抖函数
        debounce: function(func, delay) {
            let timeoutId;
            return function(...args) {
                clearTimeout(timeoutId);
                timeoutId = setTimeout(() => func.apply(this, args), delay);
            };
        },
        
        // 节流函数
        throttle: function(func, delay) {
            let lastCall = 0;
            return function(...args) {
                const now = Date.now();
                if (now - lastCall >= delay) {
                    lastCall = now;
                    func.apply(this, args);
                }
            };
        },
        
        performSearch: function() {
            if (!this.searchQuery.trim()) {
                this.searchResults = [];
                return;
            }
            
            // 模拟API调用
            fetch(`/api/search?q=${encodeURIComponent(this.searchQuery)}`)
                .then(response => response.json())
                .then(results => {
                    this.searchResults = results;
                })
                .catch(error => {
                    console.error('搜索失败:', error);
                });
        }
    },
    
    template: `
        <div class="search-input">
            <input 
                v-model="searchQuery" 
                placeholder="搜索..."
                type="text"
            >
            <div class="search-results">
                <div 
                    v-for="result in searchResults" 
                    :key="result.id"
                    class="search-result"
                >
                    {{ result.title }}
                </div>
            </div>
        </div>
    `
});

(二)列表优化

// v-show vs v-if 的性能考虑
Vue.component('conditional-rendering', {
    data: function() {
        return {
            showExpensiveComponent: false,
            toggleFrequently: false
        };
    },
    
    template: `
        <div>
            <!-- 频繁切换使用v-show -->
            <expensive-component v-show="toggleFrequently"></expensive-component>
            
            <!-- 条件很少改变使用v-if -->
            <another-component v-if="showExpensiveComponent"></another-component>
        </div>
    `
});

// 列表渲染优化
Vue.component('optimized-list', {
    data: function() {
        return {
            items: [],
            filter: ''
        };
    },
    
    computed: {
        // 使用计算属性缓存过滤结果
        filteredItems: function() {
            if (!this.filter) {
                return this.items;
            }
            return this.items.filter(item => {
                return item.name.toLowerCase().includes(this.filter.toLowerCase());
            });
        }
    },
    
    template: `
        <div>
            <input v-model="filter" placeholder="过滤项目">
            <div 
                v-for="item in filteredItems" 
                :key="item.id"
                class="list-item"
            >
                {{ item.name }}
            </div>
        </div>
    `
});

// 组件懒加载
Vue.component('lazy-component', {
    data: function() {
        return {
            shouldLoad: false
        };
    },
    
    methods: {
        loadComponent: function() {
            this.shouldLoad = true;
        }
    },
    
    template: `
        <div>
            <button v-if="!shouldLoad" @click="loadComponent">
                加载组件
            </button>
            <heavy-component v-if="shouldLoad"></heavy-component>
        </div>
    `
});

// 事件监听器优化
Vue.component('event-optimization', {
    data: function() {
        return {
            items: []
        };
    },
    
    methods: {
        // 使用事件委托而不是为每个项目绑定事件
         handleItemClick: function(event) {
             if (event.target.classList.contains('item-button')) {
                 const itemId = event.target.dataset.itemId;
                 this.processItem(itemId);
             }
         },
         
         processItem: function(itemId) {
             console.log('处理项目:', itemId);
         }
     },
     
     template: `
         <div class="event-optimization">
             <!-- 使用事件委托 -->
             <div @click="handleItemClick" class="items-container">
                 <div 
                     v-for="item in items" 
                     :key="item.id"
                     class="item"
                 >
                     {{ item.name }}
                     <button 
                         class="item-button" 
                         :data-item-id="item.id"
                     >
                         处理
                     </button>
                 </div>
             </div>
         </div>
     `
 });

八、Vue 2最佳实践

(一)项目结构

src/
├── api/                    # API接口
│   ├── index.js
│   ├── user.js
│   └── product.js
├── assets/                 # 静态资源
│   ├── images/
│   ├── icons/
│   └── fonts/
├── components/             # 公共组件
│   ├── common/            # 通用组件
│   │   ├── Button.vue
│   │   ├── Modal.vue
│   │   └── Loading.vue
│   └── business/          # 业务组件
│       ├── UserCard.vue
│       └── ProductList.vue
├── directives/            # 自定义指令
│   ├── index.js
│   └── permission.js
├── filters/               # 过滤器
│   ├── index.js
│   └── date.js
├── layouts/               # 布局组件
│   ├── DefaultLayout.vue
│   └── AdminLayout.vue
├── mixins/                # 混入
│   ├── index.js
│   └── pagination.js
├── plugins/               # 插件
│   ├── index.js
│   └── axios.js
├── router/                # 路由
│   ├── index.js
│   └── modules/
├── store/                 # 状态管理
│   ├── index.js
│   └── modules/
├── styles/                # 样式
│   ├── index.scss
│   ├── variables.scss
│   └── mixins.scss
├── utils/                 # 工具函数
│   ├── index.js
│   ├── request.js
│   └── validation.js
├── views/                 # 页面组件
│   ├── Home.vue
│   ├── About.vue
│   └── user/
├── App.vue
└── main.js

(二)编码规范

// 1. 组件命名规范
// 好的命名
Vue.component('UserProfile', { /* ... */ });
Vue.component('ProductCard', { /* ... */ });
Vue.component('ShoppingCart', { /* ... */ });

// 避免的命名
Vue.component('user', { /* ... */ });        // 太简单
Vue.component('UserProfileComponent', { /* ... */ }); // 冗余

// 2. Props定义规范
Vue.component('user-profile', {
    props: {
        // 基础类型检查
        userId: {
            type: Number,
            required: true
        },
        
        // 多类型
        userName: {
            type: [String, Number],
            default: 'Anonymous'
        },
        
        // 对象类型
        userInfo: {
            type: Object,
            default: () => ({}),
            validator: function(value) {
                return value.hasOwnProperty('name') && value.hasOwnProperty('email');
            }
        },
        
        // 数组类型
        tags: {
            type: Array,
            default: () => []
        },
        
        // 自定义验证
        status: {
            type: String,
            default: 'active',
            validator: function(value) {
                return ['active', 'inactive', 'pending'].includes(value);
            }
        }
    }
});

// 3. 事件命名规范
Vue.component('custom-input', {
    methods: {
        handleInput: function(value) {
            // 使用kebab-case命名事件
            this.$emit('input-change', value);
            this.$emit('value-updated', value);
        },
        
        handleSubmit: function() {
            this.$emit('form-submit', this.formData);
        }
    }
});

// 4. 计算属性和方法命名
Vue.component('user-list', {
    data: function() {
        return {
            users: [],
            searchTerm: ''
        };
    },
    
    computed: {
        // 计算属性使用名词
        filteredUsers: function() {
            return this.users.filter(user => 
                user.name.toLowerCase().includes(this.searchTerm.toLowerCase())
            );
        },
        
        activeUsersCount: function() {
            return this.users.filter(user => user.active).length;
        }
    },
    
    methods: {
        // 方法使用动词
        fetchUsers: function() {
            // 获取用户数据
        },
        
        updateUser: function(userId, data) {
            // 更新用户信息
        },
        
        deleteUser: function(userId) {
            // 删除用户
        }
    }
});

(三)错误处理和调试

// 1. 全局错误处理
Vue.config.errorHandler = function(err, vm, info) {
    console.error('Vue错误:', err);
    console.error('组件:', vm);
    console.error('错误信息:', info);
    
    // 发送错误报告到服务器
    if (process.env.NODE_ENV === 'production') {
        sendErrorReport({
            error: err.message,
            stack: err.stack,
            component: vm.$options.name || 'Unknown',
            info: info,
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString()
        });
    }
};

// 2. 组件错误边界
Vue.component('error-boundary', {
    data: function() {
        return {
            hasError: false,
            error: null
        };
    },
    
    errorCaptured: function(err, instance, info) {
        this.hasError = true;
        this.error = {
            message: err.message,
            stack: err.stack,
            info: info
        };
        
        console.error('捕获到子组件错误:', err);
        
        // 返回false阻止错误继续传播
        return false;
    },
    
    methods: {
        retry: function() {
            this.hasError = false;
            this.error = null;
            this.$forceUpdate();
        }
    },
    
    template: `
        <div>
            <div v-if="hasError" class="error-boundary">
                <h3>出现了错误</h3>
                <p>{{ error.message }}</p>
                <button @click="retry">重试</button>
                <details v-if="$root.debug">
                    <summary>错误详情</summary>
                    <pre>{{ error.stack }}</pre>
                </details>
            </div>
            <slot v-else></slot>
        </div>
    `
});

// 3. 开发环境调试工具
if (process.env.NODE_ENV === 'development') {
    // 启用Vue DevTools
    Vue.config.devtools = true;
    
    // 性能追踪
    Vue.config.performance = true;
    
    // 添加全局调试方法
    Vue.prototype.$log = function(message, data) {
        console.group('🐛 ' + message);
        if (data) {
            console.log('数据:', data);
        }
        console.log('组件:', this.$options.name || 'Anonymous');
        console.log('路由:', this.$route ? this.$route.path : 'No route');
        console.groupEnd();
    };
}

九、总结

Vue 2作为一个渐进式前端框架,具有以下核心优势:

## (一)主要特点

1. 渐进式架构:可以逐步引入,从简单的页面增强到复杂的单页应用
2. 响应式数据绑定:基于Object.defineProperty的响应式系统
3. 组件化开发:高度可复用的组件系统
4. 虚拟DOM:高效的DOM更新机制
5. 丰富的生态系统:Vue Router、Vuex、Vue CLI等官方工具

## (二)适用场景

- 中小型项目:快速开发,学习成本低
- 渐进式改造:可以逐步替换现有项目的部分功能
- 原型开发:快速验证想法和概念
- 企业级应用:配合完整的工具链可以构建大型应用

## (三)最佳实践总结

1. 项目结构:保持清晰的目录结构和命名规范
2. 组件设计:遵循单一职责原则,保持组件的可复用性
3. 状态管理:合理使用Vuex管理应用状态
4. 性能优化:使用计算属性、虚拟滚动、代码分割等技术
5. 错误处理:建立完善的错误处理和监控机制

## (四)发展趋势

虽然Vue 3已经发布,但Vue 2仍然是一个稳定可靠的选择:

- 长期支持:Vue 2将继续维护到2023年底
- 生态成熟:拥有丰富的第三方库和工具
- 学习价值:理解Vue 2有助于更好地掌握Vue 3
- 项目迁移:为未来升级到Vue 3打下基础

Vue 2以其简洁的API、优秀的性能和完善的生态系统,为前端开发者提供了一个优秀的开发体验。无论是初学者还是经验丰富的开发者,都能在Vue 2中找到适合自己的开发方式。

本文详细介绍了Vue 2的核心概念、实用技巧和最佳实践,希望能够帮助开发者更好地理解和使用这个优秀的前端框架。