目录
(2)输入组件(components/TodoInput.vue)
(3)列表项组件(components/TodoItem.vue)
(4)列表组件(components/TodoList.vue)
(5)筛选组件(components/TodoFilter.vue)
Vue.js(简称 Vue)是一套用于构建用户界面的渐进式 JavaScript 框架。自 2014 年由尤雨溪发布以来,凭借其简洁的 API、优秀的性能和灵活的集成能力,成为前端开发的主流框架之一。本文将从基础概念到实战开发,全面解析 Vue 的核心原理与应用。
一、Vue 框架核心思想与优势
1. 核心设计理念
Vue 的核心思想可以概括为两点:数据驱动和组件化。
数据驱动(Data-Driven):
传统 DOM 操作需要手动将数据同步到视图,而 Vue 通过数据绑定机制,使视图自动响应数据变化。当数据更新时,视图会自动重新渲染,无需开发者编写 DOM 操作代码。组件化(Component-Based):
将页面拆分为独立、可复用的组件(Component),每个组件包含自身的结构(HTML)、样式(CSS)和逻辑(JavaScript)。组件化让代码更易维护、复用性更高。
2. 与其他框架的对比
框架 | 特点 | 适用场景 |
---|---|---|
Vue | 渐进式框架,API 简洁,学习曲线平缓 | 中小型应用、快速开发、与现有项目集成 |
React | 基于 JSX,生态庞大,灵活性高 | 大型复杂应用、需要高度定制化的场景 |
Angular | 全功能框架,TypeScript 友好,规范严格 | 企业级大型应用、团队协作开发 |
3. 为什么选择 Vue?
- 易学易用:HTML、CSS、JavaScript 基础开发者可快速上手
- 性能出色:采用虚拟 DOM 和响应式系统,渲染效率高
- 灵活性强:可作为库引入现有项目,也可构建完整单页应用
- 完善生态:配套工具(Vue CLI、Vue Router、Pinia)和社区支持丰富
二、Vue 基础环境搭建
1. 快速入门:直接引入 Vue
适合小型项目或学习测试:
<!-- 引入Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">
{{ message }}
</div>
<script>
// 创建Vue实例
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
2. 工程化开发:Vue CLI
适合中大型项目,提供完整的开发环境配置:
(1)安装 Vue CLI
# 全局安装Vue
CLI npm install -g @vue/cli
# 检查版本
vue --version
(2)创建项目
# 创建项目
vue create my-vue-app
# 选择预设(推荐手动选择特性)
# 选择Vue 3、Babel、Router、Vuex等必要组件
(3)运行项目
cd my-vue-app
npm run serve
访问http://localhost:8080
即可看到默认页面。
三、Vue 核心概念与基础语法
1. Vue 实例与数据绑定
Vue 应用的入口是通过createApp
创建的应用实例,所有功能都基于这个实例展开。
<div id="app">
<h1>{{ title }}</h1>
<p>{{ user.name }} - {{ user.age }}岁</p>
<p>{{ isActive ? '在线' : '离线' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</div>
<script>
const app = Vue.createApp({
// 数据对象,返回应用所需的响应式数据
data() {
return {
title: 'Vue基础示例',
user: {
name: '张三',
age: 25
},
isActive: true,
message: 'Hello'
}
}
})
// 将应用挂载到DOM元素上
app.mount('#app')
</script>
{{ }}
:插值表达式,用于将数据渲染到视图中- 支持 JavaScript 表达式(如三元运算、字符串方法)
- 数据是响应式的:当数据变化时,视图会自动更新
2. 指令系统
Vue 提供了一系列特殊属性(指令),用于在模板中实现逻辑控制。
(1)v-bind
:绑定属性
<!-- 完整语法 -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- 简写形式(推荐) -->
<img :src="imageUrl" :alt="imageAlt">
data() {
return {
imageUrl: 'logo.png',
imageAlt: '网站logo'
}
}
(2)v-model
:双向数据绑定
主要用于表单元素,实现数据与视图的双向同步:
<div id="app">
<input type="text" v-model="username">
<p>你输入的是:{{ username }}</p>
<textarea v-model="content"></textarea>
<select v-model="selectedCity">
<option value="">请选择</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
</select>
</div>
<script>
Vue.createApp({
data() {
return {
username: '',
content: '',
selectedCity: ''
}
}
}).mount('#app')
</script>
(3)v-for
:列表渲染
用于循环渲染数组或对象:
<ul>
<!-- 遍历数组 -->
<li v-for="(item, index) in fruits" :key="index">
{{ index + 1 }}. {{ item }}
</li>
</ul>
<table border="1">
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
<!-- 遍历对象数组 -->
<tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
</tr>
</table>
data() {
return {
fruits: ['苹果', '香蕉', '橙子'],
users: [
{ id: 1, name: '张三', age: 20 },
{ id: 2, name: '李四', age: 22 }
]
}
}
:key
:提高渲染性能,确保每个节点的唯一性
(4)v-if
与v-show
:条件渲染
<!-- v-if:条件不满足时元素会从DOM中移除 -->
<div v-if="isLogin">欢迎回来,{{ username }}!</div>
<div v-else>请先登录</div>
<!-- 多条件判断 -->
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
<!-- v-show:条件不满足时元素隐藏(通过display: none) -->
<div v-show="isActive">这是可以显示/隐藏的内容</div>
data() {
return {
isLogin: true,
username: '张三',
score: 85,
isActive: false
}
}
- 区别:
v-if
有更高的切换开销,v-show
有更高的初始渲染开销- 频繁切换用
v-show
,条件很少改变用v-if
(5)v-on
:事件绑定
<!-- 完整语法 -->
<button v-on:click="handleClick">点击我</button>
<!-- 简写形式(推荐) -->
<button @click="handleClick">点击我</button>
<!-- 传递参数 -->
<button @click="sayHello('Vue')">打招呼</button>
<!-- 事件修饰符 -->
<button @click.stop="handleClick">阻止冒泡</button>
<input @keyup.enter="handleSubmit">
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count++
},
sayHello(name) {
alert(`Hello ${name}!`)
},
handleSubmit() {
console.log('提交表单')
}
}
- 事件处理函数定义在
methods
选项中this
指向当前 Vue 实例- 事件修饰符:
.stop
(阻止冒泡)、.prevent
(阻止默认行为)、.enter
(按键修饰符)等
3. 计算属性与侦听器
(1)计算属性(computed)
用于处理复杂逻辑计算,具有缓存特性:
<div id="app">
<p>原始价格:{{ price }}</p>
<p>折扣价:{{ discountedPrice }}</p>
<p>总价:{{ totalPrice }}</p>
</div>
<script>
Vue.createApp({
data() {
return {
price: 100,
quantity: 3,
discount: 0.8
}
},
computed: {
// 计算折扣价
discountedPrice() {
return this.price * this.discount
},
// 计算总价
totalPrice() {
return this.discountedPrice * this.quantity
}
}
}).mount('#app')
</script>
- 计算属性会基于依赖的数据自动更新
- 相比方法(methods),计算属性有缓存,多次访问不会重复计算
(2)侦听器(watch)
用于监听数据变化并执行副作用操作:
<div id="app">
<input v-model="username">
<p>{{ message }}</p>
</div>
<script>
Vue.createApp({
data() {
return {
username: '',
message: ''
}
},
watch: {
// 监听username变化
username(newVal, oldVal) {
if (newVal.length < 3) {
this.message = '用户名长度不能少于3个字符'
} else {
this.message = '用户名可用'
}
}
}
}).mount('#app')
</script>
- 适合处理异步操作或复杂逻辑(如数据请求、定时器等)
四、Vue 组件化开发
组件是 Vue 的核心概念,将页面拆分为独立可复用的模块。
1. 组件的定义与注册
(1)全局组件
在整个应用中都可使用:
// 定义全局组件
const app = Vue.createApp({})
app.component('my-component', {
template: `
<div class="my-component">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
`,
data() {
return {
title: '我是全局组件',
content: '这是全局组件的内容'
}
}
})
app.mount('#app')
使用组件:
<div id="app">
<my-component></my-component>
</div>
(2)局部组件
仅在注册它的父组件中可用:
// 定义局部组件
const MyComponent = {
template: `
<div>
<p>{{ message }}</p>
</div>
`,
data() {
return {
message: '我是局部组件'
}
}
}
// 在父组件中注册
const app = Vue.createApp({
components: {
'my-component': MyComponent
}
})
app.mount('#app')
2. 组件通信
(1)父组件向子组件传值(props)
javascript
// 子组件
const ChildComponent = {
// 声明接收的props
props: ['title', 'userInfo', 'isActive'],
template: `
<div>
<h3>{{ title }}</h3>
<p>{{ userInfo.name }} - {{ userInfo.age }}岁</p>
<p v-if="isActive">激活状态</p>
</div>
`
}
// 父组件
const app = Vue.createApp({
components: {
'child-component': ChildComponent
},
data() {
return {
parentTitle: '来自父组件的标题',
user: {
name: '张三',
age: 25
},
active: true
}
}
})
使用时传递数据:
<child-component
:title="parentTitle"
:user-info="user"
:is-active="active"
></child-component>
- props 命名:在组件中使用驼峰式(camelCase),在模板中使用短横线式(kebab-case)
- 可以指定 props 类型和验证:
props: {
// 基础类型检查
age: Number,
// 多个可能的类型
id: [String, Number],
// 必传且是字符串
name: {
type: String,
required: true
},
// 带默认值的数字
count: {
type: Number,
default: 0
}
}
(2)子组件向父组件传值($emit)
// 子组件
const ChildComponent = {
template: `
<button @click="handleClick">点击发送消息</button>
`,
methods: {
handleClick() {
// 触发自定义事件,并传递数据
this.$emit('send-message', '来自子组件的消息', 123)
}
}
}
// 父组件
const app = Vue.createApp({
components: {
'child-component': ChildComponent
},
methods: {
// 接收子组件传递的消息
receiveMessage(msg, num) {
console.log('收到消息:', msg, num)
}
}
})
父组件监听事件:
<child-component @send-message="receiveMessage"></child-component>
3. 插槽(Slot)
用于组件内容分发,使组件更灵活:
// 子组件(MyButton)
const MyButton = {
template: `
<button class="my-button">
<!-- 插槽:父组件传递的内容会放在这里 -->
<slot></slot>
</button>
`
}
// 父组件使用
const app = Vue.createApp({
components: {
'my-button': MyButton
}
})
使用带插槽的组件:
<my-button>
<span>点击我</span>
</my-button>
<my-button>
<i class="icon"></i> 提交
</my-button>
具名插槽(多个插槽区分):
// 子组件
const Card = {
template: `
<div class="card">
<div class="card-header">
<slot name="header"></slot>
</div>
<div class="card-body">
<slot></slot> <!-- 默认插槽 -->
</div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
`
}
使用具名插槽:
<card>
<template v-slot:header>
<h3>卡片标题</h3>
</template>
<p>这是卡片内容...</p>
<template v-slot:footer>
<button>确定</button>
</template>
</card>
五、Vue 底层原理
1. 响应式系统原理
Vue 的响应式系统基于数据劫持和依赖收集实现:
初始化阶段:
- Vue 会遍历 data 中的所有属性,使用
Object.defineProperty
(Vue 2)或Proxy
(Vue 3)进行劫持- 为每个属性创建对应的
Dep
(依赖管理器),用于收集依赖依赖收集:
- 当组件渲染时,会触发属性的 getter 方法
- getter 方法会将当前组件的
Watcher
(观察者)添加到Dep
中数据更新:
- 当属性被修改时,会触发 setter 方法
- setter 方法会通知
Dep
中的所有Watcher
Watcher
触发组件重新渲染
Vue 3 使用 Proxy 替代 Object.defineProperty 的优势:
- 支持监听数组变化
- 支持监听新增 / 删除属性
- 性能更好,劫持整个对象而非单个属性
2. 虚拟 DOM 与 Diff 算法
Vue 通过虚拟 DOM 提高渲染性能:
虚拟 DOM:
- 用 JavaScript 对象模拟真实 DOM 结构
- 例如:
{ tag: 'div', props: { id: 'app' }, children: [] }
Diff 算法:
- 当数据变化时,Vue 会生成新的虚拟 DOM
- 通过 Diff 算法对比新旧虚拟 DOM 的差异
- 只更新差异部分对应的真实 DOM,减少 DOM 操作
key 的作用:
- 在列表渲染中,key 帮助 Vue 识别元素的唯一性
- 提高 Diff 算法的效率,避免不必要的元素重绘
六、Vue Router 路由管理
Vue Router 是 Vue 官方的路由管理器,用于构建单页应用(SPA)。
1. 安装与配置
# 安装Vue Router(Vue 3对应版本)
npm install vue-router@4
创建路由配置(router/index.js
):
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
history: createWebHistory(), // HTML5 history模式
routes
})
export default router
在 main.js 中引入:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App)
.use(router)
.mount('#app')
2. 路由使用
在 App.vue 中添加路由出口和导航:
<template>
<div id="app">
<!-- 路由导航 -->
<nav>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</nav>
<!-- 路由出口:匹配的组件将在这里渲染 -->
<router-view></router-view>
</div>
</template>
3. 动态路由与参数传递
// 路由配置
{
path: '/user/:id',
name: 'User',
component: User
}
在组件中获取参数:
// User.vue
export default {
mounted() {
// 通过$route获取路由参数
console.log(this.$route.params.id)
}
}
编程式导航:
// 导航到用户页面
this.$router.push({
name: 'User',
params: { id: 123 }
})
// 后退
this.$router.go(-1)
七、实战项目:待办事项应用(Todo App)
1. 项目功能设计
- 添加待办事项
- 标记待办事项为已完成
- 删除待办事项
- 筛选待办事项(全部 / 已完成 / 未完成)
- 统计待办事项数量
2. 项目结构
todo-app/
├── public/
├── src/
│ ├── components/
│ │ ├── TodoInput.vue // 输入组件
│ │ ├── TodoList.vue // 列表组件
│ │ ├── TodoItem.vue // 列表项组件
│ │ └── TodoFilter.vue // 筛选组件
│ ├── stores/
│ │ └── todoStore.js // 状态管理
│ ├── App.vue
│ └── main.js
└── package.json
3. 核心代码实现
(1)状态管理(stores/todoStore.js)
import { defineStore } from 'pinia'
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: [
{ id: 1, text: '学习Vue', completed: false },
{ id: 2, text: '完成项目', completed: true }
],
filter: 'all' // all, active, completed
}),
getters: {
// 筛选后的待办事项
filteredTodos(state) {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed)
case 'completed':
return state.todos.filter(todo => todo.completed)
default:
return state.todos
}
},
// 未完成数量
activeCount(state) {
return state.todos.filter(todo => !todo.completed).length
}
},
actions: {
// 添加待办
addTodo(text) {
if (!text.trim()) return
this.todos.push({
id: Date.now(),
text,
completed: false
})
},
// 切换完成状态
toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
},
// 删除待办
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
// 设置筛选条件
setFilter(filter) {
this.filter = filter
}
}
})
(2)输入组件(components/TodoInput.vue)
<template>
<input
type="text"
v-model="newTodo"
@keyup.enter="handleAdd"
placeholder="请输入待办事项..."
>
<button @click="handleAdd">添加</button>
</template>
<script>
import { useTodoStore } from '../stores/todoStore'
export default {
data() {
return {
newTodo: ''
}
},
methods: {
handleAdd() {
const todoStore = useTodoStore()
todoStore.addTodo(this.newTodo)
this.newTodo = '' // 清空输入框
}
}
}
</script>
(3)列表项组件(components/TodoItem.vue)
<template>
<li :class="{ completed: todo.completed }">
<span @click="$emit('toggle', todo.id)">{{ todo.text }}</span>
<button @click="$emit('delete', todo.id)">删除</button>
</li>
</template>
<script>
export default {
props: {
todo: {
type: Object,
required: true
}
}
}
</script>
<style>
.completed {
text-decoration: line-through;
color: #999;
}
</style>
(4)列表组件(components/TodoList.vue)
<template>
<ul>
<todo-item
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@delete="deleteTodo"
></todo-item>
</ul>
<p v-if="filteredTodos.length === 0">没有待办事项</p>
</template>
<script>
import TodoItem from './TodoItem.vue'
import { useTodoStore } from '../stores/todoStore'
export default {
components: {
TodoItem
},
computed: {
filteredTodos() {
return useTodoStore().filteredTodos
}
},
methods: {
toggleTodo(id) {
useTodoStore().toggleTodo(id)
},
deleteTodo(id) {
useTodoStore().deleteTodo(id)
}
}
}
</script>
(5)筛选组件(components/TodoFilter.vue)
<template>
<div>
<span>筛选:</span>
<button
:class="{ active: currentFilter === 'all' }"
@click="$emit('filter', 'all')"
>
全部
</button>
<button
:class="{ active: currentFilter === 'active' }"
@click="$emit('filter', 'active')"
>
未完成
</button>
<button
:class="{ active: currentFilter === 'completed' }"
@click="$emit('filter', 'completed')"
>
已完成
</button>
<span>剩余:{{ activeCount }} 项</span>
</div>
</template>
<script>
import { useTodoStore } from '../stores/todoStore'
export default {
props: {
currentFilter: {
type: String,
required: true
}
},
computed: {
activeCount() {
return useTodoStore().activeCount
}
}
}
</script>
<style>
.active {
background-color: #42b983;
color: white;
}
</style>
(6)主组件(App.vue)
<template>
<div id="app">
<h1>待办事项</h1>
<todo-input></todo-input>
<todo-list></todo-list>
<todo-filter
:current-filter="filter"
@filter="handleFilter"
></todo-filter>
</div>
</template>
<script>
import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'
import TodoFilter from './components/TodoFilter.vue'
import { useTodoStore } from './stores/todoStore'
export default {
components: {
TodoInput,
TodoList,
TodoFilter
},
computed: {
filter() {
return useTodoStore().filter
}
},
methods: {
handleFilter(filter) {
useTodoStore().setFilter(filter)
}
}
}
</script>
4. 项目运行与测试
# 安装依赖
npm install
# 运行项目
npm run serve
访问http://localhost:8080
即可使用待办事项应用,测试各功能是否正常工作:
- 输入框中输入内容,按回车或点击添加按钮
- 点击待办事项文本切换完成状态
- 点击删除按钮移除待办事项
- 切换筛选按钮查看不同状态的待办事项
- 检查剩余数量统计是否正确