《uni-app跨平台开发完全指南》- 03 - Vue.js基础入门

Vue.js 基础

本系列是《uni-app跨平台开发完全指南》系列教程,旨在帮助开发者从零开始掌握uni-app开发。本章将深入讲解Vue.js的核心概念,为你后续的uni-app开发打下坚实基础。

为什么学习Vue.js对uni-app开发如此重要?

很多初学者可能会问:"我直接学uni-app不行吗?为什么要先学Vue.js?"

这里有个很重要的概念需要理解:uni-app的本质是基于Vue.js的跨端实现框架。更形象一点,如果说uni-app是整车制造,那么Vue.js就属于发动机。如果你不懂发动机原理,虽然也能开车,但一旦出现故障,就束手无策了。同样,不掌握Vue.js基础,在uni-app开发中遇到复杂问题时,你会很难找到根本解决方案。

一、Vue.js 简介与开发环境搭建

1.1 Vue.js 是什么?

简单来说,Vue.js是一个用于构建用户界面的渐进式JavaScript框架。所谓"渐进式",意味着你可以根据项目需求,逐步采用Vue.js的特性:

  • 可以在老项目中局部使用Vue.js增强交互
  • 也可以使用Vue.js全家桶开发完整的前端应用
  • 还可以用Vue.js开发原生移动应用(如uni-app)

1.2 环境准备:第一个Vue应用

让我们从最简单的HTML页面开始:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的第一个Vue应用</title>
    <!-- 引入Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <!-- Vue实例挂载点 -->
    <div id="app">
        <h1>{{ message }}</h1>
        <button @click="reverseMessage">反转消息</button>
    </div>

    <script>
        // 创建Vue实例
        var app = new Vue({
            el: '#app', // 指定挂载元素
            data: {     // 定义数据
                message: 'Hello Vue!'
            },
            methods: {  // 定义方法
                reverseMessage: function() {
                    this.message = this.message.split('').reverse().join('');
                }
            }
        });
    </script>
</body>
</html>

代码解析

  • el: '#app':告诉Vue这个实例要控制页面中id为app的元素
  • data:定义这个Vue实例的数据,可以在模板中使用
  • {{ message }}:模板语法,将data中的message值渲染到页面
  • @click:事件绑定,点击时执行reverseMessage方法

二、Vue 核心概念

2.1 数据绑定

数据绑定是Vue最核心的特性之一,它建立了数据DOM之间的自动同步关系。

2.1.1 文本插值:{{ }}
html 复制代码
<div id="app">
    <!-- 基本文本插值 -->
    <p>消息:{{ message }}</p>
    
    <!-- JS表达式 -->
    <p>计算:{{ number + 1 }}</p>
    <p>三元表达式:{{ isActive ? '激活' : '未激活' }}</p>
    <p>反转:{{ message.split('').reverse().join('') }}</p>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue!',
        number: 10,
        isActive: true
    }
});
</script>

重要提示{{ }}中支持JavaScript表达式,但不支持语句(如iffor等)。

2.1.2 属性绑定:v-bind
html 复制代码
<div id="app">
    <!-- 绑定HTML属性 -->
    <div v-bind:title="tooltip">鼠标悬停查看提示</div>
    
    <!-- 绑定CSS类 -->
    <div v-bind:class="{ active: isActive, 'text-danger': hasError }">
        动态类名
    </div>
    
    <!-- 绑定样式 -->
    <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">
        动态样式
    </div>
    
    <!-- 简写 -->
    <img :src="imageSrc" :alt="imageAlt">
</div>

<script>
new Vue({
    el: '#app',
    data: {
        tooltip: '这是一个提示信息',
        isActive: true,
        hasError: false,
        activeColor: 'red',
        fontSize: 20,
        imageSrc: 'path/to/image.jpg',
        imageAlt: '示例图片'
    }
});
</script>

v-bind原理:当数据变化时,Vue会自动更新对应的DOM属性。

2.2 指令系统

指令是带有v-前缀的特殊属性,它们为HTML添加了动态行为。

2.2.1 条件渲染:v-if vs v-show
html 复制代码
<div id="app">
    <!-- v-if:条件性地渲染一块内容 -->
    <p v-if="score >= 90">优秀!</p>
    <p v-else-if="score >= 60">及格</p>
    <p v-else>不及格</p>
    
    <!-- v-show:总是渲染,只是切换display -->
    <p v-show="isVisible">这个元素会显示/隐藏</p>
    
    <button @click="toggle">切换显示</button>
    <button @click="changeScore">改变分数</button>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        score: 85,
        isVisible: true
    },
    methods: {
        toggle: function() {
            this.isVisible = !this.isVisible;
        },
        changeScore: function() {
            this.score = Math.floor(Math.random() * 100);
        }
    }
});
</script>

v-if 与 v-show 的区别

特性 v-if v-show
渲染方式 条件为false时不渲染DOM元素 总是渲染,只是切换display属性
切换开销 更高的切换开销(创建/销毁组件) 更高的初始渲染开销
适用场景 运行时条件很少改变 需要非常频繁地切换
2.2.2 列表渲染:v-for
html 复制代码
<div id="app">
    <!-- 遍历数组 -->
    <ul>
        <li v-for="(item, index) in items" :key="item.id">
            {{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
        </li>
    </ul>
    
    <!-- 遍历对象 -->
    <ul>
        <li v-for="(value, key) in userInfo" :key="key">
            {{ key }}: {{ value }}
        </li>
    </ul>
    
    <!-- 遍历数字范围 -->
    <span v-for="n in 5" :key="n">{{ n }} </span>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        items: [
            { id: 1, name: '苹果', price: 5 },
            { id: 2, name: '香蕉', price: 3 },
            { id: 3, name: '橙子', price: 4 }
        ],
        userInfo: {
            name: '张三',
            age: 25,
            city: '北京'
        }
    }
});
</script>

关键点

  • :key的重要性:为每个节点提供唯一标识,优化列表渲染性能
  • 可以使用(item, index)(value, key, index)语法
2.2.3 事件处理:v-on
html 复制代码
<div id="app">
    <!-- 基本事件处理 -->
    <button v-on:click="counter += 1">点击次数: {{ counter }}</button>
    
    <!-- 方法事件处理器 -->
    <button @click="sayHello">打招呼</button>
    
    <!-- 内联处理器中的方法 -->
    <button @click="say('Hello', $event)">带参数的事件</button>
    
    <!-- 事件修饰符 -->
    <form @submit.prevent="onSubmit">
        <input type="text">
        <button type="submit">提交</button>
    </form>
    
    <!-- 按键修饰符 -->
    <input @keyup.enter="onEnter" placeholder="按回车键触发">
</div>

<script>
new Vue({
    el: '#app',
    data: {
        counter: 0
    },
    methods: {
        sayHello: function(event) {
            alert('Hello!');
            console.log(event); // 原生事件对象
        },
        say: function(message, event) {
            alert(message);
            if (event) {
                event.preventDefault();
            }
        },
        onSubmit: function() {
            alert('表单提交被阻止了!');
        },
        onEnter: function() {
            alert('你按了回车键!');
        }
    }
});
</script>

常用事件修饰符

  • .stop:阻止事件冒泡
  • .prevent:阻止默认行为
  • .capture:使用事件捕获模式
  • .self:只当事件是从侦听器绑定的元素本身触发时才触发回调
  • .once:只触发一次
  • .passive:告诉浏览器你不想阻止事件的默认行为
2.2.4 双向数据绑定:v-model
html 复制代码
<div id="app">
    <!-- 文本输入 -->
    <input v-model="message" placeholder="编辑我">
    <p>消息是: {{ message }}</p>
    
    <!-- 多行文本 -->
    <textarea v-model="multilineText"></textarea>
    <p style="white-space: pre-line;">{{ multilineText }}</p>
    
    <!-- 复选框 -->
    <input type="checkbox" id="checkbox" v-model="checked">
    <label for="checkbox">{{ checked ? '已选中' : '未选中' }}</label>
    
    <!-- 多个复选框 -->
    <div>
        <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
        <label for="jack">Jack</label>
        <input type="checkbox" id="john" value="John" v-model="checkedNames">
        <label for="john">John</label>
        <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
        <label for="mike">Mike</label>
        <br>
        <span>选中的名字: {{ checkedNames }}</span>
    </div>
    
    <!-- 单选按钮 -->
    <div>
        <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>
        <br>
        <span>选中的值: {{ picked }}</span>
    </div>
    
    <!-- 选择框 -->
    <select v-model="selected">
        <option disabled value="">请选择</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
    </select>
    <span>选中的值: {{ selected }}</span>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        message: '',
        multilineText: '',
        checked: false,
        checkedNames: [],
        picked: '',
        selected: ''
    }
});
</script>

v-model原理:本质上是语法糖,它负责监听用户的输入事件以更新数据。

javascript 复制代码
// v-model 相当于:
<input 
  :value="message" 
  @input="message = $event.target.value">

2.3 计算属性与监听器

2.3.1 计算属性:computed
html 复制代码
<div id="app">
    <input v-model="firstName" placeholder="姓">
    <input v-model="lastName" placeholder="名">
    
    <!-- 使用计算属性 -->
    <p>全名(计算属性): {{ fullName }}</p>
    
    <!-- 使用方法 -->
    <p>全名(方法): {{ getFullName() }}</p>
    
    <!-- 示例代码 -->
    <div>
        <h3>购物车</h3>
        <div v-for="item in cart" :key="item.id">
            {{ item.name }} - ¥{{ item.price }} × {{ item.quantity }}
        </div>
        <p>总价: {{ totalPrice }}</p>
        <p>打折后: {{ discountedTotal }}</p>
    </div>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        firstName: '张',
        lastName: '三',
        cart: [
            { id: 1, name: '商品A', price: 100, quantity: 2 },
            { id: 2, name: '商品B', price: 200, quantity: 1 }
        ],
        discount: 0.8 // 8折
    },
    computed: {
        // 计算属性:基于依赖进行缓存
        fullName: function() {
            console.log('计算属性 fullName 被调用了');
            return this.firstName + ' ' + this.lastName;
        },
        // 计算总价
        totalPrice: function() {
            return this.cart.reduce((total, item) => {
                return total + (item.price * item.quantity);
            }, 0);
        },
        // 基于其他计算属性的计算属性
        discountedTotal: function() {
            return this.totalPrice * this.discount;
        }
    },
    methods: {
        // 方法:每次重新渲染都会调用
        getFullName: function() {
            console.log('方法 getFullName 被调用了');
            return this.firstName + ' ' + this.lastName;
        }
    }
});
</script>

计算属性的依赖追踪流程:

graph TD A[访问计算属性] --> B{脏数据?} B -->|是| C[重新计算值] B -->|否| D[返回缓存值] C --> E[标记为干净数据] E --> D F[依赖数据变化] --> G[标记为脏数据] G --> A

计算属性特点

  • 基于它们的响应式依赖进行缓存
  • 只在相关响应式依赖发生改变时才会重新求值
  • 多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数
2.3.2 监听器:watch
html 复制代码
<div id="app">
    <input v-model="question" placeholder="输入问题">
    <p>答案: {{ answer }}</p>
    
    <!-- 示例 -->
    <input v-model="user.name" placeholder="用户名">
    <input v-model="user.age" type="number" placeholder="年龄">
    <p>用户信息变化次数: {{ changeCount }}</p>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        question: '',
        answer: '我无法给你答案直到你提问!',
        user: {
            name: '',
            age: 0
        },
        changeCount: 0
    },
    watch: {
        // 简单监听:question发生变化时执行
        question: function(newQuestion, oldQuestion) {
            this.answer = '等待你停止输入...';
            this.getAnswer();
        },
        // 深度监听:对象内部属性的变化
        user: {
            handler: function(newVal, oldVal) {
                this.changeCount++;
                console.log('用户信息发生变化:', newVal);
            },
            deep: true, 
            immediate: true 
        }
    },
    methods: {
        getAnswer: function() {
            // 模拟异步操作
            setTimeout(() => {
                this.answer = '这是对你问题的回答';
            }, 1000);
        }
    }
});
</script>

计算属性 vs 监听器

场景 使用计算属性 使用监听器
数据派生 适用于现有数据计算新数据 不适用
异步操作 不支持异步 支持异步
性能优化 自动缓存 无缓存
复杂逻辑 声明式 命令式

三、组件化开发

组件化就像搭积木一样,把复杂的界面拆分成独立、可复用的部分。

3.1 组件注册与使用

3.1.1 全局组件
html 复制代码
<div id="app">
    <!-- 使用全局组件 -->
    <my-button></my-button>
    <user-card 
        name="张三" 
        :age="25" 
        avatar="path/to/avatar.jpg">
    </user-card>
</div>

<script>
// 全局组件注册
Vue.component('my-button', {
    template: `
        <button class="my-btn" @click="onClick">
            <slot>默认按钮</slot>
        </button>
    `,
    methods: {
        onClick: function() {
            this.$emit('btn-click'); // 触发自定义事件
        }
    }
});

// 另一个全局组件
Vue.component('user-card', {
    props: ['name', 'age', 'avatar'], // 定义组件属性
    template: `
        <div class="user-card">
            <img :src="avatar" :alt="name" class="avatar">
            <div class="info">
                <h3>{{ name }}</h3>
                <p>年龄: {{ age }}</p>
            </div>
        </div>
    `
});

new Vue({
    el: '#app'
});
</script>

<style>
.my-btn {
    padding: 10px 20px;
    background: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.user-card {
    border: 1px solid #ddd;
    padding: 15px;
    margin: 10px 0;
    border-radius: 8px;
    display: flex;
    align-items: center;
}

.avatar {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    margin-right: 15px;
}

.info h3 {
    margin: 0 0 5px 0;
}
</style>
3.1.2 局部组件
html 复制代码
<div id="app">
    <product-list></product-list>
</div>

<script>
// 定义局部组件
var ProductList = {
    template: `
        <div class="product-list">
            <h2>商品列表</h2>
            <product-item 
                v-for="product in products" 
                :key="product.id"
                :product="product"
                @add-to-cart="onAddToCart">
            </product-item>
        </div>
    `,
    data: function() {
        return {
            products: [
                { id: 1, name: 'iPhone', price: 5999, stock: 10 },
                { id: 2, name: 'MacBook', price: 9999, stock: 5 },
                { id: 3, name: 'iPad', price: 3299, stock: 8 }
            ]
        };
    },
    methods: {
        onAddToCart: function(product) {
            console.log('添加到购物车:', product.name);
            // 这里可以调用Vuex或触发全局事件
        }
    }
};

// 子组件
var ProductItem = {
    props: ['product'],
    template: `
        <div class="product-item">
            <h3>{{ product.name }}</h3>
            <p>价格: ¥{{ product.price }}</p>
            <p>库存: {{ product.stock }}</p>
            <button 
                @click="addToCart" 
                :disabled="product.stock === 0">
                {{ product.stock === 0 ? '缺货' : '加入购物车' }}
            </button>
        </div>
    `,
    methods: {
        addToCart: function() {
            this.$emit('add-to-cart', this.product);
        }
    }
};

new Vue({
    el: '#app',
    components: {
        'product-list': ProductList,
        'product-item': ProductItem
    }
});
</script>

<style>
.product-list {
    max-width: 600px;
    margin: 0 auto;
}

.product-item {
    border: 1px solid #eee;
    padding: 15px;
    margin: 10px 0;
    border-radius: 5px;
}

.product-item h3 {
    color: #333;
    margin-top: 0;
}
</style>

3.2 组件通信

组件通信是组件化开发的核心,Vue提供了多种通信方式,用一张图来看下:

graph TB A[组件通信] --> B[父子通信] A --> C[兄弟通信] A --> D[跨级通信] A --> E[全局通信] B --> B1[Props Down] B --> B2[Events Up] B --> B3[v-model] B --> B4[refs] C --> C1[Event Bus] C --> C2[共同父级] D --> D1[Provide/Inject] D --> D2[attrs/listeners] E --> E1[Vuex] E --> E2[全局事件]

下面结合一段具体代码示例,带大家了解下组件间是如何通信的:

html 复制代码
<div id="app">
    <h2>组件通信示例</h2>
    
    <!-- 1. 父子组件 -->
    <parent-component></parent-component>
    
    <!-- 2. 事件总线 -->
    <component-a></component-a>
    <component-b></component-b>
</div>

<script>
// 事件总线(用于非父子组件通信)
var eventBus = new Vue();

// 组件A
Vue.component('component-a', {
    template: `
        <div class="component">
            <h3>组件A</h3>
            <button @click="sendMessage">发送消息给组件B</button>
        </div>
    `,
    methods: {
        sendMessage: function() {
            eventBus.$emit('message-from-a', '你好,这是来自组件A的消息!');
        }
    }
});

// 组件B
Vue.component('component-b', {
    template: `
        <div class="component">
            <h3>组件B</h3>
            <p>收到消息: {{ receivedMessage }}</p>
        </div>
    `,
    data: function() {
        return {
            receivedMessage: ''
        };
    },
    mounted: function() {
        var self = this;
        eventBus.$on('message-from-a', function(message) {
            self.receivedMessage = message;
        });
    }
});

// 父组件
Vue.component('parent-component', {
    template: `
        <div class="parent">
            <h3>父组件</h3>
            <p>父组件数据: {{ parentData }}</p>
            
            <!-- 父传子:通过props -->
            <child-component 
                :message="parentData"
                @child-event="onChildEvent">
            </child-component>
            
            <!-- 子传父:通过自定义事件 -->
            <p>子组件消息: {{ childMessage }}</p>
        </div>
    `,
    data: function() {
        return {
            parentData: '来自父组件的数据',
            childMessage: ''
        };
    },
    methods: {
        onChildEvent: function(message) {
            this.childMessage = message;
        }
    }
});

// 子组件
Vue.component('child-component', {
    props: ['message'], // 接收父组件数据
    template: `
        <div class="child">
            <h4>子组件</h4>
            <p>收到父组件的消息: {{ message }}</p>
            <button @click="sendToParent">发送消息给父组件</button>
        </div>
    `,
    methods: {
        sendToParent: function() {
            this.$emit('child-event', '来自子组件的问候!');
        }
    }
});

new Vue({
    el: '#app'
});
</script>

<style>
.component, .parent, .child {
    border: 1px solid #ccc;
    padding: 15px;
    margin: 10px;
    border-radius: 5px;
}

.parent {
    background: #f0f8ff;
}

.child {
    background: #f9f9f9;
    margin-left: 30px;
}
</style>

四、生命周期函数

4.1 生命周期

Vue实例有一个完整的生命周期,包括创建、挂载、更新、销毁等阶段。每个阶段都提供了相应的生命周期钩子,让我们可以在特定阶段执行自定义逻辑。

sequenceDiagram participant P as Parent Component participant C as Child Component participant VD as Virtual DOM participant RD as Real DOM Note over P: 1. 父组件创建 P->>C: 2. 创建子组件实例 Note over C: 3. beforeCreate Note over C: 4. 初始化注入 Note over C: 5. created C->>VD: 6. 编译模板为渲染函数 Note over C: 7. beforeMount C->>RD: 8. 创建$el并挂载 Note over C: 9. mounted Note over C: 10. 等待数据变化 C->>C: 11. 数据变化 Note over C: 12. beforeUpdate C->>VD: 13. 重新渲染 VD->>RD: 14. 打补丁 Note over C: 15. updated P->>C: 16. 销毁子组件 Note over C: 17. beforeDestroy C->>C: 18. 清理工作 Note over C: 19. destroyed

其实生命周期钩子函数不用刻意去记忆,实在不知道直接控制台打印看日志结果就行了,当然能记住最好~~~

4.2 生命周期钩子

html 复制代码
<div id="app">
    <h2>用计时器来演示生命周期狗子函数</h2>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加</button>
    <button @click="destroy">销毁实例</button>
    
    <div v-if="showChild">
        <lifecycle-demo :count="count"></lifecycle-demo>
    </div>
    <button @click="showChild = !showChild">切换子组件</button>
</div>

<script>
Vue.component('lifecycle-demo', {
    props: ['count'],
    template: `
        <div class="lifecycle-demo">
            <h3>子组件 - 计数: {{ count }}</h3>
            <p>生命周期调用记录:</p>
            <ul>
                <li v-for="log in logs" :key="log.id">{{ log.message }}</li>
            </ul>
        </div>
    `,
    data: function() {
        return {
            logs: [],
            logId: 0
        };
    },
    
    // 生命周期钩子
    beforeCreate: function() {
        this.addLog('beforeCreate: 实例刚被创建,data和methods还未初始化');
    },
    
    created: function() {
        this.addLog('created: 实例创建完成,data和methods已初始化');
        // 这里可以调用API获取初始数据
        this.fetchData();
    },
    
    beforeMount: function() {
        this.addLog('beforeMount: 模板编译完成,但尚未挂载到页面');
    },
    
    mounted: function() {
        this.addLog('mounted: 实例已挂载到DOM元素,可以访问$el');
        // 这里可以操作DOM或初始化第三方库
        this.initializeThirdPartyLib();
    },
    
    beforeUpdate: function() {
        this.addLog('beforeUpdate: 数据更新前,虚拟DOM重新渲染之前');
    },
    
    updated: function() {
        this.addLog('updated: 数据更新完成,DOM已重新渲染');
        // 这里可以执行依赖于DOM更新的操作
    },
    
    beforeDestroy: function() {
        this.addLog('beforeDestroy: 实例销毁前,此时实例仍然完全可用');
        // 这里可以清理定时器、取消订阅等
        this.cleanup();
    },
    
    destroyed: function() {
        // 注意:在destroyed钩子中无法添加日志,因为组件已销毁
        console.log('destroyed: 实例已销毁,所有绑定和监听器已被移除');
    },
    
    methods: {
        addLog: function(message) {
            this.logs.push({
                id: this.logId++,
                message: message + ' - ' + new Date().toLocaleTimeString()
            });
        },
        
        fetchData: function() {
            // 模拟接口请求
            setTimeout(() => {
                this.addLog('数据获取完成');
            }, 100);
        },
        
        initializeThirdPartyLib: function() {
            this.addLog('三方库初始化完成');
        },
        
        cleanup: function() {
            this.addLog('清理工作完成');
        }
    }
});

new Vue({
    el: '#app',
    data: {
        count: 0,
        showChild: true
    },
    methods: {
        destroy: function() {
            this.$destroy();
            alert('Vue实例被销毁');
        }
    }
});
</script>

<style>
.lifecycle-demo {
    border: 2px solid #4CAF50;
    padding: 15px;
    margin: 10px 0;
    background: #f9fff9;
}

.lifecycle-demo ul {
    max-height: 200px;
    overflow-y: auto;
    background: white;
    padding: 10px;
    border: 1px solid #ddd;
}
</style>

4.3 生命周期使用场景总结

生命周期钩子 常见使用场景
created - API数据请求 - 事件监听器初始化 - 定时器设置
mounted - DOM操作 - 三方库初始化(如图表库) - 插件初始化
updated - DOM依赖的操作 - 基于新状态的操作
beforeDestroy - 清除定时器 - 取消事件监听 - 清理三方库实例

五、响应式原理

响应式就是当数据发生变化时,视图会自动更新。这听起来很简单,但底层原理有着巧妙的设计。

5.1 原理

Vue的响应式系统基于三个核心概念:

5.1.1 数据劫持(Object.defineProperty)
javascript 复制代码
// 简化的响应式原理
function defineReactive(obj, key, val) {
    // 递归处理嵌套对象
    observe(val);
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            console.log(`读取 ${key}: ${val}`);
            // 这里会进行依赖收集
            return val;
        },
        set: function(newVal) {
            if (newVal === val) return;
            console.log(`设置 ${key}: ${newVal}`);
            val = newVal;
            // 这里会通知依赖更新
            updateView();
        }
    });
}

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

// 测试
const data = { message: 'Hello', count: 0 };
observe(data);

// 现在data是响应式的
data.message = 'Hello Vue!';  // 控制台会输出:设置 message: Hello Vue!
console.log(data.message);    // 控制台会输出:读取 message: Hello Vue!
5.1.2 依赖收集与派发更新

Vue的响应式系统实际更为复杂,包含依赖收集和派发更新机制:

javascript 复制代码
// 简化的Dep(依赖)类
class Dep {
    constructor() {
        this.subscribers = new Set();
    }
    
    depend() {
        if (activeUpdate) {
            this.subscribers.add(activeUpdate);
        }
    }
    
    notify() {
        this.subscribers.forEach(sub => sub());
    }
}

let activeUpdate = null;

function autorun(update) {
    function wrappedUpdate() {
        activeUpdate = wrappedUpdate;
        update();
        activeUpdate = null;
    }
    wrappedUpdate();
}

// 使用示例
const dep = new Dep();

autorun(() => {
    dep.depend();
    console.log('更新视图');
});

// 当数据变化时
dep.notify(); // 输出:更新视图

5.2 注意事项

5.2.1 数组更新检测
html 复制代码
<div id="app">
    <h3>数组响应式注意事项</h3>
    <ul>
        <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
    
    <button @click="addItem">添加项目successed</button>
    <button @click="addItemWrong">添加项目error</button>
    <button @click="changeItemProperty">修改项目属性</button>
</div>

<script>
new Vue({
    el: '#app',
    data: {
        items: [
            { id: 1, name: '项目1' },
            { id: 2, name: '项目2' }
        ]
    },
    methods: {
        // 推荐使用数组变异方法
        addItem: function() {
            this.items.push({
                id: this.items.length + 1,
                name: '项目' + (this.items.length + 1)
            });
        },
        
        // 不推荐直接通过索引设置
        addItemWrong: function() {
            // 这种方式不会触发视图更新!
            this.items[this.items.length] = {
                id: this.items.length + 1,
                name: '项目' + (this.items.length + 1)
            };
            console.log('数组已修改,但视图不会更新');
        },
        
        // 对象属性的响应式
        changeItemProperty: function() {
            // Vue.set 或 this.$set 确保响应式
            this.$set(this.items[0], 'newProperty', '新属性值');
        }
    }
});
</script>
5.2.2 响应式API
javascript 复制代码
// 响应式API
new Vue({
    data: {
        user: {
            name: '张三'
        },
        list: [1, 2, 3]
    },
    created() {
        // 添加响应式属性
        this.$set(this.user, 'age', 25);
        
        // 删除响应式属性
        this.$delete(this.user, 'name');
        
        // 数组响应式方法
        this.list = this.$set(this.list, 0, 100); // 替换第一个元素
        
        // 或者使用Vue.set全局方法
        Vue.set(this.list, 1, 200);
    }
});

六、项目实战:TodoList应用

用一个完整的TodoList应用来综合运用以上所学知识:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue.js TodoList应用</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .todo-app {
            max-width: 500px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px 20px;
            text-align: center;
        }
        .header h1 {
            margin-bottom: 10px;
            font-size: 2.5em;
        }
        .input-section {
            padding: 20px;
            border-bottom: 1px solid #eee;
        }
        .todo-input {
            width: 100%;
            padding: 15px;
            border: 2px solid #e1e1e1;
            border-radius: 8px;
            font-size: 16px;
            transition: border-color 0.3s;
        }
        .todo-input:focus {
            outline: none;
            border-color: #667eea;
        }
        .add-btn {
            width: 100%;
            padding: 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            cursor: pointer;
            margin-top: 10px;
            transition: transform 0.2s;
        }
        .add-btn:hover {
            transform: translateY(-2px);
        }
        .filters {
            display: flex;
            padding: 15px 20px;
            border-bottom: 1px solid #eee;
        }
        .filter-btn {
            flex: 1;
            padding: 10px;
            background: none;
            border: none;
            cursor: pointer;
            transition: all 0.3s;
            border-radius: 5px;
            margin: 0 5px;
        }
        .filter-btn.active {
            background: #667eea;
            color: white;
        }
        .todo-list {
            max-height: 400px;
            overflow-y: auto;
        }
        .todo-item {
            display: flex;
            align-items: center;
            padding: 15px 20px;
            border-bottom: 1px solid #f1f1f1;
            transition: background-color 0.3s;
        }
        .todo-item:hover {
            background-color: #f9f9f9;
        }
        .todo-item.completed {
            opacity: 0.6;
        }
        .todo-item.completed .todo-text {
            text-decoration: line-through;
        }
        .todo-checkbox {
            margin-right: 15px;
            transform: scale(1.2);
        }
        .todo-text {
            flex: 1;
            font-size: 16px;
        }
        .delete-btn {
            background: #ff4757;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .delete-btn:hover {
            background: #ff3742;
        }
        .stats {
            padding: 15px 20px;
            text-align: center;
            color: #666;
            border-top: 1px solid #eee;
        }
        .empty-state {
            text-align: center;
            padding: 40px 20px;
            color: #999;
        }
    </style>
</head>
<body>
    <div id="app">
        <div class="todo-app">
            <!-- 头部 -->
            <div class="header">
                <h1>TodoList</h1>
                <p>Vue.js应用</p>
            </div>
            
            <div class="input-section">
                <input 
                    v-model="newTodo" 
                    @keyup.enter="addTodo"
                    placeholder="添加新任务..."
                    class="todo-input">
                <button @click="addTodo" class="add-btn">
                    添加任务
                </button>
            </div>
            
            <!-- 过滤器 -->
            <div class="filters">
                <button 
                    @click="filter = 'all'"
                    :class="['filter-btn', { active: filter === 'all' }]">
                    全部 ({{ totalTodos }})
                </button>
                <button 
                    @click="filter = 'active'"
                    :class="['filter-btn', { active: filter === 'active' }]">
                    待完成 ({{ activeTodos }})
                </button>
                <button 
                    @click="filter = 'completed'"
                    :class="['filter-btn', { active: filter === 'completed' }]">
                    已完成 ({{ completedTodos }})
                </button>
            </div>
            
            <!-- Todo列表 -->
            <div class="todo-list">
                <div v-if="filteredTodos.length === 0" class="empty-state">
                    {{ emptyMessage }}
                </div>
                
                <div 
                    v-for="todo in filteredTodos" 
                    :key="todo.id"
                    :class="['todo-item', { completed: todo.completed }]">
                    
                    <input 
                        type="checkbox" 
                        v-model="todo.completed"
                        class="todo-checkbox">
                    
                    <span class="todo-text">{{ todo.text }}</span>
                    
                    <button 
                        @click="removeTodo(todo.id)"
                        class="delete-btn">
                        删除
                    </button>
                </div>
            </div>
            
            <!-- 统计信息 -->
            <div class="stats">
                <span v-if="totalTodos > 0">
                    进度: {{ completionRate }}% ({{ completedTodos }}/{{ totalTodos }})
                </span>
                <span v-else>还没有任务,添加一个吧!</span>
            </div>
        </div>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                newTodo: '',           // 新任务输入
                todos: [],             // 任务列表
                filter: 'all',         // 当前过滤器
                nextId: 1              // 下一个任务ID
            },
            
            // 计算属性
            computed: {
                // 总任务数
                totalTodos() {
                    return this.todos.length;
                },
                
                // 活跃任务数
                activeTodos() {
                    return this.todos.filter(todo => !todo.completed).length;
                },
                
                // 已完成任务数
                completedTodos() {
                    return this.todos.filter(todo => todo.completed).length;
                },
                
                // 完成率
                completionRate() {
                    if (this.totalTodos === 0) return 0;
                    return Math.round((this.completedTodos / this.totalTodos) * 100);
                },
                
                // 过滤后的任务列表
                filteredTodos() {
                    switch (this.filter) {
                        case 'active':
                            return this.todos.filter(todo => !todo.completed);
                        case 'completed':
                            return this.todos.filter(todo => todo.completed);
                        default:
                            return this.todos;
                    }
                },
                
                // 空状态消息
                emptyMessage() {
                    switch (this.filter) {
                        case 'active':
                            return '没有待完成的任务!';
                        case 'completed':
                            return '还没有完成的任务!';
                        default:
                            return '还没有任务,添加一个吧!';
                    }
                }
            },
            
            methods: {
                // 添加新任务
                addTodo() {
                    if (this.newTodo.trim() === '') return;
                    
                    this.todos.push({
                        id: this.nextId++,
                        text: this.newTodo.trim(),
                        completed: false,
                        createdAt: new Date()
                    });
                    
                    this.newTodo = ''; 
                },
                
                // 删除任务
                removeTodo(id) {
                    this.todos = this.todos.filter(todo => todo.id !== id);
                }
            },
            
            // 生命周期钩子
            created() {
                console.log('TodoList应用已创建');
                // 加载本地存储的数据。。。
            },
            
            mounted() {
                console.log('TodoList应用已挂载');
            }
        });
    </script>
</body>
</html>

这个TodoList应用综合运用了:v-model@click@keyupv-ifv-forcomputed:class生命周期钩子

七、总结

7.1 核心概念

  1. 数据驱动:Vue的核心思想,数据变化自动更新视图
  2. 指令系统:v-bind, v-model, v-for, v-if等指令的强大功能
  3. 组件化:将UI拆分为独立可复用的组件
  4. 生命周期:理解组件从创建到销毁的完整过程
  5. 响应式原理:理解数据变化的侦测机制

7.2 组件设计原则

javascript 复制代码
// 好的组件设计
Vue.component('user-profile', {
    props: {
        user: {
            type: Object,
            required: true,
            validator: function(value) {
                return value.name && value.email;
            }
        }
    },
    template: `
        <div class="user-profile">
            <img :src="user.avatar" :alt="user.name">
            <h3>{{ user.name }}</h3>
            <p>{{ user.email }}</p>
        </div>
    `
});

// 不好的组件设计(props验证不足,模板混乱)
Vue.component('bad-component', {
    props: ['user'],
    template: '<div>...</div>' // 模板过长,难以维护
});

7.3 状态管理建议

javascript 复制代码
// 对于复杂应用,考虑使用Vuex
// 对于简单应用,合理组织组件间通信

// 好的状态组织
new Vue({
    data: {
        // 相关状态分组
        user: {
            profile: {},
            preferences: {}
        },
        ui: {
            loading: false,
            sidebarOpen: true
        }
    }
});

7.4 常见问题

常见问题 错误做法 正确做法
数组更新 this.items[0] = newValue this.$set(this.items, 0, newValue)
对象属性 this.obj.newProp = value this.$set(this.obj, 'newProp', value)
异步更新 直接操作DOM 使用this.$nextTick()
事件监听 不清理事件监听器 beforeDestroy中清理

结语

至此Vue.js基础就学习完了,想要掌握更多的Vue.js知识可去官网深入学习,掌握好Vue.js,uni-app学习就会事半功倍。


如果觉得本文对你有帮助,请一键三连(点赞、关注、收藏)支持一下!有任何问题欢迎在评论区留言讨论。

相关推荐
掘金012 小时前
在 Vue 3 项目中使用 MQTT 获取数据
前端·javascript·vue.js
一 乐2 小时前
个人理财系统|基于java+小程序+APP的个人理财系统设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·后端·小程序
wyzqhhhh2 小时前
同时打开两个浏览器页面,关闭 A 页面的时候,要求 B 页面同时关闭,怎么实现?
前端·javascript·react.js
晴殇i2 小时前
从 WebSocket 到 SSE:实时通信的轻量化演进
前端·javascript
网络点点滴2 小时前
reactive创建对象类型的响应式数据
前端·javascript·vue.js
携欢3 小时前
PortSwigger靶场之盲 SSRF(服务器端请求伪造)漏洞通关秘籍
前端·网络·安全·web安全
慧慧吖@3 小时前
前端关于埋点
前端
universe_013 小时前
前端学习css
前端·css·学习
熊猫比分站3 小时前
[特殊字符] Java/Vue 实现体育比分直播系统,支持多端实时更新
java·开发语言·vue.js