Vue2 基础入门
Vue2 基础入门:从零开始构建你的第一个应用
作为一名从 jQuery 时代走过来的前端开发者,第一次接触 Vue 时的感受至今记忆犹新。那种"原来前端还能这么写"的惊喜,让我彻底爱上了这个框架。本文将带你从零开始,深入理解 Vue2 的核心思想和基础用法。
为什么选择 Vue2?
在正式开始之前,我想先聊聊为什么在 Vue3 已经发布的今天,我们仍然需要学习 Vue2。
现实考量:
- 大量企业级项目仍在使用 Vue2,维护这些项目是刚需
- Vue2 的生态更加成熟稳定,Element UI、Ant Design Vue 1.x 等组件库依然活跃
- 理解 Vue2 有助于更好地掌握 Vue3 的改进之处
技术层面:
- Vue2 的 Options API 更容易理解和上手
- 对于中小型项目,Vue2 完全够用
- Vue2.7 版本已经支持了 Composition API,可以平滑过渡
环境搭建
方式一:CDN 引入(学习推荐)
<!DOCTYPE html>
<html>
<head>
<title>Vue2 入门</title>
<!-- 开发环境版本,包含完整的警告和调试模式 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="app">{{ message }}</div>
<script>
new Vue({
el: "#app",
data: {
message: "Hello Vue!",
},
});
</script>
</body>
</html>
方式二:Vue CLI(项目开发推荐)
# 安装 Vue CLI
npm install -g @vue/cli@4.5.15
# 创建项目
vue create my-project
# 进入项目目录
cd my-project
# 启动开发服务器
npm run serve
Vue CLI 4.x 是 Vue2 项目的最佳选择,它提供了完善的项目脚手架和配置选项。
Vue 实例:一切的起点
每个 Vue 应用都是从创建一个 Vue 实例开始的:
const vm = new Vue({
// 选项
});
这里的 vm(ViewModel 的缩写)代表 Vue 实例,它是连接视图和数据的桥梁。
完整的实例选项
new Vue({
el: "#app", // 挂载元素
data: {
// 响应式数据
count: 0,
user: { name: "张三", age: 25 },
},
computed: {
// 计算属性
doubleCount() {
return this.count * 2;
},
},
methods: {
// 方法
increment() {
this.count++;
},
},
watch: {
// 侦听器
count(newVal, oldVal) {
console.log(`count 从 ${oldVal} 变成了 ${newVal}`);
},
},
// 生命周期钩子
created() {
console.log("实例创建完成");
},
mounted() {
console.log("DOM 挂载完成");
},
});
模板语法:声明式渲染的魅力
Vue 使用基于 HTML 的模板语法,允许你声明式地将数据绑定到 DOM。
插值表达式
<template>
<!-- 文本插值 -->
<span>消息: {{ msg }}</span>
<!-- 原始 HTML(慎用,防止 XSS) -->
<span v-html="rawHtml"></span>
<!-- JavaScript 表达式 -->
<span>{{ number + 1 }}</span>
<span>{{ ok ? '是' : '否' }}</span>
<span>{{ message.split('').reverse().join('') }}</span>
</template>
注意:模板表达式中只能访问有限的全局变量,如 Math 和 Date,不能访问用户自定义的全局变量。
指令系统
指令是带有 v- 前缀的特殊属性,它们的职责是当表达式的值改变时,响应式地作用于 DOM。
<template>
<!-- v-bind:动态绑定属性 -->
<a v-bind:href="url">链接</a>
<a :href="url">链接(缩写)</a>
<!-- v-on:事件监听 -->
<button v-on:click="doSomething">点击</button>
<button @click="doSomething">点击(缩写)</button>
<!-- v-model:双向数据绑定 -->
<input v-model="message" />
<!-- v-if/v-else-if/v-else:条件渲染 -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
<!-- v-show:显示/隐藏(CSS 切换) -->
<div v-show="isVisible">可见内容</div>
<!-- v-for:列表渲染 -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
</template>
v-if 与 v-show 的选择
这是一个经典问题,我来分享一下实际项目中的经验:
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件为假时不渲染 DOM | 始终渲染,通过 CSS 控制显示 |
| 切换开销 | 较高(DOM 操作) | 较低(CSS 切换) |
| 初始开销 | 条件为假时无开销 | 始终有初始渲染开销 |
| 适用场景 | 不频繁切换 | 频繁切换 |
实际建议:
- 需要频繁切换显示状态的元素(如 Tab 面板),用
v-show - 运行时条件很少改变的情况,用
v-if - 需要配合
v-else使用时,只能用v-if
数据绑定:响应式的核心
Vue 最独特的特性之一是其响应式系统。当数据发生变化时,视图会自动更新。
data 选项
// 组件中 data 必须是函数
export default {
data() {
return {
message: "Hello",
user: {
name: "张三",
age: 25,
},
items: [1, 2, 3],
};
},
};
为什么组件的 data 必须是函数?
这是为了保证每个组件实例都有独立的数据副本。如果 data 是对象,多个组件实例会共享同一份数据,造成数据污染。
响应式原理简述
Vue2 使用 Object.defineProperty() 来实现响应式:
// 简化版原理示意
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取 ${key}`);
return val;
},
set(newVal) {
console.log(`设置 ${key} = ${newVal}`);
val = newVal;
// 触发视图更新
updateView();
},
});
}
响应式的注意事项
Vue 无法检测以下变动:
// ❌ 直接通过索引设置数组项
this.items[0] = "new value";
// ✅ 正确做法
this.$set(this.items, 0, "new value");
// 或
this.items.splice(0, 1, "new value");
// ❌ 直接添加对象属性
this.user.email = "test@example.com";
// ✅ 正确做法
this.$set(this.user, "email", "test@example.com");
// 或
this.user = { ...this.user, email: "test@example.com" };
计算属性与侦听器
计算属性 computed
计算属性是基于它的响应式依赖进行缓存的:
export default {
data() {
return {
firstName: "张",
lastName: "三",
items: [
{ name: "苹果", price: 10, quantity: 2 },
{ name: "香蕉", price: 5, quantity: 3 },
],
};
},
computed: {
// 简单的 getter
fullName() {
return this.firstName + this.lastName;
},
// getter 和 setter
fullNameWithSetter: {
get() {
return this.firstName + this.lastName;
},
set(newValue) {
const names = newValue.split(" ");
this.firstName = names[0];
this.lastName = names[1];
},
},
// 实际案例:购物车总价
totalPrice() {
return this.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
},
},
};
侦听器 watch
当需要在数据变化时执行异步操作或开销较大的操作时,使用侦听器:
export default {
data() {
return {
searchQuery: "",
user: {
name: "张三",
address: {
city: "北京",
},
},
};
},
watch: {
// 简单侦听
searchQuery(newVal, oldVal) {
this.debouncedSearch();
},
// 立即执行
searchQuery: {
handler(newVal) {
this.search();
},
immediate: true,
},
// 深度侦听
user: {
handler(newVal) {
console.log("user 变化了");
},
deep: true,
},
// 侦听对象的某个属性
"user.address.city"(newVal) {
console.log("城市变成了:", newVal);
},
},
methods: {
debouncedSearch: _.debounce(function () {
// 搜索逻辑
}, 300),
},
};
computed vs watch 的选择
| 场景 | 推荐 |
|---|---|
| 依赖多个数据计算出一个结果 | computed |
| 一个数据变化需要执行多个操作 | watch |
| 需要异步操作 | watch |
| 需要缓存计算结果 | computed |
事件处理
事件绑定基础
<template>
<!-- 直接调用方法 -->
<button @click="handleClick">点击</button>
<!-- 传递参数 -->
<button @click="handleClick('hello')">点击</button>
<!-- 访问原生事件对象 -->
<button @click="handleClick($event)">点击</button>
<!-- 内联处理 -->
<button @click="count++">{{ count }}</button>
</template>
<script>
export default {
methods: {
handleClick(msg, event) {
console.log(msg);
if (event) {
event.preventDefault();
}
},
},
};
</script>
事件修饰符
Vue 提供了一系列事件修饰符,让我们可以更优雅地处理事件:
<!-- 阻止冒泡 -->
<div @click.stop="handleClick"></div>
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit"></form>
<!-- 只触发一次 -->
<button @click.once="doThis"></button>
<!-- 捕获模式 -->
<div @click.capture="handleClick"></div>
<!-- 只有点击元素自身时触发 -->
<div @click.self="handleClick"></div>
<!-- 修饰符串联 -->
<a @click.stop.prevent="handleClick"></a>
<!-- 按键修饰符 -->
<input @keyup.enter="submit" />
<input @keyup.esc="cancel" />
<input @keyup.ctrl.enter="submit" />
表单处理
v-model 是 Vue 中处理表单的利器,它实现了表单元素与数据的双向绑定。
基础用法
<template>
<form @submit.prevent="handleSubmit">
<!-- 文本输入 -->
<input v-model="form.username" placeholder="用户名" />
<!-- 多行文本 -->
<textarea v-model="form.bio"></textarea>
<!-- 单选框 -->
<input type="radio" v-model="form.gender" value="male" /> 男
<input type="radio" v-model="form.gender" value="female" /> 女
<!-- 复选框 - 单个(布尔值) -->
<input type="checkbox" v-model="form.agree" /> 同意协议
<!-- 复选框 - 多个(数组) -->
<input type="checkbox" v-model="form.hobbies" value="reading" /> 阅读
<input type="checkbox" v-model="form.hobbies" value="gaming" /> 游戏
<input type="checkbox" v-model="form.hobbies" value="sports" /> 运动
<!-- 下拉选择 -->
<select v-model="form.city">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
<button type="submit">提交</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
username: "",
bio: "",
gender: "",
agree: false,
hobbies: [],
city: "",
},
};
},
methods: {
handleSubmit() {
console.log(this.form);
},
},
};
</script>
v-model 修饰符
<!-- .lazy:在 change 事件后同步(而非 input) -->
<input v-model.lazy="msg" />
<!-- .number:将输入转为数字 -->
<input v-model.number="age" type="number" />
<!-- .trim:去除首尾空格 -->
<input v-model.trim="username" />
生命周期
理解 Vue 的生命周期对于编写高质量的组件至关重要。
生命周期图示
创建阶段
├── beforeCreate → 实例初始化后,数据观测和事件配置之前
├── created → 实例创建完成,可访问 data、methods 等
│
挂载阶段
├── beforeMount → 挂载开始前,render 函数首次被调用
├── mounted → 实例挂载完成,DOM 可访问
│
更新阶段
├── beforeUpdate → 数据更新时,DOM 打补丁之前
├── updated → DOM 更新完成
│
销毁阶段
├── beforeDestroy → 实例销毁前,实例仍然可用
└── destroyed → 实例销毁后
各阶段的典型用法
export default {
data() {
return {
timer: null,
};
},
beforeCreate() {
// 此时 data 和 methods 还不可用
// 一般用于插件开发中执行初始化任务
},
created() {
// 可以访问 data 和 methods
// 适合发起异步请求获取初始数据
this.fetchData();
},
beforeMount() {
// $el 还不可用
},
mounted() {
// DOM 已经渲染完成
// 适合操作 DOM、初始化第三方库
this.initChart();
// 设置定时器
this.timer = setInterval(() => {
this.refreshData();
}, 5000);
},
beforeUpdate() {
// 可以在这里访问更新前的 DOM
},
updated() {
// DOM 已更新
// 避免在这里修改数据,可能导致无限循环
},
beforeDestroy() {
// 清理工作的最佳时机
// 清除定时器、取消事件监听、取消订阅等
if (this.timer) {
clearInterval(this.timer);
}
window.removeEventListener("resize", this.handleResize);
},
destroyed() {
// 实例已被销毁
},
};
父子组件生命周期执行顺序
这是面试常考点,也是实际开发中需要注意的:
挂载阶段:
父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted
更新阶段:
父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
销毁阶段:
父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed
实战:Todo List 应用
让我们通过一个完整的 Todo List 来巩固所学知识:
<template>
<div class="todo-app">
<h1>我的待办事项</h1>
<!-- 输入区域 -->
<div class="input-area">
<input
v-model.trim="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
/>
<button @click="addTodo" :disabled="!newTodo">添加</button>
</div>
<!-- 过滤器 -->
<div class="filters">
<button
v-for="f in filters"
:key="f.value"
:class="{ active: filter === f.value }"
@click="filter = f.value"
>
{{ f.label }}
</button>
</div>
<!-- 列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.done }"
>
<input type="checkbox" v-model="todo.done" />
<span v-if="!todo.editing" @dblclick="startEdit(todo)">
{{ todo.text }}
</span>
<input
v-else
v-model="todo.text"
@blur="finishEdit(todo)"
@keyup.enter="finishEdit(todo)"
@keyup.esc="cancelEdit(todo)"
/>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计信息 -->
<div class="stats" v-if="todos.length">
<span>{{ remaining }} 项未完成</span>
<button v-show="todos.length > remaining" @click="clearCompleted">
清除已完成
</button>
</div>
</div>
</template>
<script>
export default {
name: "TodoApp",
data() {
return {
newTodo: "",
filter: "all",
filters: [
{ label: "全部", value: "all" },
{ label: "未完成", value: "active" },
{ label: "已完成", value: "completed" },
],
todos: [],
editCache: "",
};
},
computed: {
filteredTodos() {
switch (this.filter) {
case "active":
return this.todos.filter((t) => !t.done);
case "completed":
return this.todos.filter((t) => t.done);
default:
return this.todos;
}
},
remaining() {
return this.todos.filter((t) => !t.done).length;
},
},
watch: {
todos: {
handler(newTodos) {
localStorage.setItem("todos", JSON.stringify(newTodos));
},
deep: true,
},
},
created() {
const saved = localStorage.getItem("todos");
if (saved) {
this.todos = JSON.parse(saved);
}
},
methods: {
addTodo() {
if (!this.newTodo) return;
this.todos.push({
id: Date.now(),
text: this.newTodo,
done: false,
editing: false,
});
this.newTodo = "";
},
removeTodo(id) {
const index = this.todos.findIndex((t) => t.id === id);
if (index > -1) {
this.todos.splice(index, 1);
}
},
startEdit(todo) {
this.editCache = todo.text;
todo.editing = true;
},
finishEdit(todo) {
if (!todo.text.trim()) {
this.removeTodo(todo.id);
}
todo.editing = false;
},
cancelEdit(todo) {
todo.text = this.editCache;
todo.editing = false;
},
clearCompleted() {
this.todos = this.todos.filter((t) => !t.done);
},
},
};
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.completed span {
text-decoration: line-through;
color: #999;
}
.filters button.active {
background: #42b983;
color: white;
}
</style>
总结
本文介绍了 Vue2 的核心基础知识,包括:
- Vue 实例:一切的起点,配置选项的容器
- 模板语法:声明式渲染,让数据驱动视图
- 数据绑定:响应式系统,自动更新视图
- 计算属性和侦听器:处理复杂逻辑的两种方式
- 事件处理:优雅的事件系统和修饰符
- 表单处理:v-model 实现双向绑定
- 生命周期:组件从创建到销毁的完整过程
这些是 Vue2 开发的基石,掌握它们之后,你就可以开始构建简单的应用了。下一篇文章,我们将深入探讨 Vue2 的组件系统,这是 Vue 最强大的特性之一。
延伸阅读
- Vue2 官方文档:https://v2.vuejs.org/
- Vue CLI 文档:https://cli.vuejs.org/
- Vue Devtools 浏览器扩展