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表达式,但不支持语句(如if、for等)。
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>
计算属性的依赖追踪流程:
计算属性特点:
- 基于它们的响应式依赖进行缓存
- 只在相关响应式依赖发生改变时才会重新求值
- 多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数
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提供了多种通信方式,用一张图来看下:
下面结合一段具体代码示例,带大家了解下组件间是如何通信的:
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实例有一个完整的生命周期,包括创建、挂载、更新、销毁等阶段。每个阶段都提供了相应的生命周期钩子,让我们可以在特定阶段执行自定义逻辑。
其实生命周期钩子函数不用刻意去记忆,实在不知道直接控制台打印看日志结果就行了,当然能记住最好~~~
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、@keyup、v-if、v-for、computed、:class、生命周期钩子
七、总结
7.1 核心概念
- 数据驱动:Vue的核心思想,数据变化自动更新视图
- 指令系统:v-bind, v-model, v-for, v-if等指令的强大功能
- 组件化:将UI拆分为独立可复用的组件
- 生命周期:理解组件从创建到销毁的完整过程
- 响应式原理:理解数据变化的侦测机制
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学习就会事半功倍。
如果觉得本文对你有帮助,请一键三连(点赞、关注、收藏)支持一下!有任何问题欢迎在评论区留言讨论。