前言:前面期末周突然加了一个项目作业,感觉不是开发能力而是vibecoding能力,想写一个工具与vue有关的,但之前没有系统学习过,在微信公众号里面看了几篇好文章,然后真自己实际上手起来的话还是挺困难的,因此本篇作为一个按照官方给的一个快速上手教程来,默认是
.vue结尾的设置,在官方基础上进行的一些个人的尝试和思考
开始
在教程最开始的时候给出了一段代码:

这里的h1好理解,就是html里面的一级标题,然后有一个template:
在 Vue.js 中,<template> 是组件的模板容器,用于包裹该组件所有的 HTML 结构:
它不是原生 HTML 标签:虽然原生 HTML5 也有一个 <template> 标签(用于存放客户端不渲染的惰性内容),但在 Vue 的单文件组件(.vue)中,它是 Vue 编译器专属的语法标记
并且不会被渲染到 DOM 中:Vue 编译后,<template> 标签本身会消失,只有里面的内容(如 <h1>Hello World!</h1>)才会被真正渲染到页面上
那么在此基础上可以增加多级标题或具体内容,可以自行尝试:

声明式渲染
reactive和ref
教程中直接给出了相应的解释:
Vue 的核心功能是声明式渲染:通过扩展于标准 HTML 的模板语法,我们可以根据 JavaScript 的状态来描述 HTML 应该是什么样子的。当状态改变时,HTML 会自动更新
能在改变时触发更新的状态被称作是响应式的,这里可以看官方教程或者我下面的代码:
js
// 普通对象:改了数据,页面不动
let user = { name: 'Shorekiten' }
user.name = 'New Name' // 页面无变化,需操作DOM
// 响应式对象:改了数据,页面自动刷新
import { reactive } from 'vue'
const user = reactive({ name: 'Shorekiten' })
user.name = 'New Name' // 页面上显示的名字自动变成 "New Name"
代码的细节放到后面再做解释,先继续跟着教程往下走,可以看到一句话:由 reactive() 创建的对象都是 JavaScript Proxy
Proxy 是JavaScript ES6 的原生特性,相当于给对象加了一层"拦截器/代理"。当访问或修改对象的属性时,Proxy 会先拦截这个操作,然后执行额外的逻辑(比如通知 Vue 更新页面),最后再把操作传递给原始对象。
然后reactive调用的是对象类型 (Object、Array、Map、Set),刚开始看到这个很懵,那就先来了解一下基本类型,即单个的、不可再分的值,直接存在内存栈里:
- string(字符串):'hello'
- number(数字):42
- boolean(布尔值):true / false
- null / undefined
- symbol / bigint
这些基本类型就是通过ref来进行调用,ref 会返回一个包裹对象,并在 .value 属性下暴露内部值。
直接这么说有点抽象,那就分开来,首先得知道什么是包裹对象:
JavaScript 的 基本类型是按值传递的,无法被 Proxy 拦截。
js
// 基本类型无法被监听
let count = 0
count = 1 // Vue 根本感知不到这个赋值操作
// 必须包一层对象,才能用 .value 的 setter 来触发更新
const count = ref(0)
// 内部等价于: { _value: 0, get value(){...}, set value(v){...} }
ref() 就是把基本类型装进去,让它变成可以被 Vue 监控的对象。
而.value就是访问的入口,然后在模板中会自动解包所以无需再加value
下面的是对象类型【引用】:
| 类型 | 说明 | 示例 |
|---|---|---|
| Object | 普通对象/字面量对象 | { name: 'Shorekiten', age: 0 } |
| Array | 数组(本质上是特殊的对象) | ['apple', 'banana'] |
| Map | 键值对集合(key 可以是任意类型) | new Map([['name', 'Shorekiten']]) |
| Set | 不重复值的集合 | new Set([1, 2, 3]) |
了解了reactive ref之后就是动态渲染文本,即双花括号语法动态渲染文本
结合
将教程中的语言结合起来构造出的结果就是这样:
js
<script setup>
import { reactive, ref } from 'vue'
const counter = reactive({ count: 0 })
const message = ref('Hello World!')
</script>
<template>
<h1>{{ message }}</h1>
<p>Count is: {{ counter.count }}</p>
</template>
首先这里的const即常量,在 Vue 3 <script setup> 中,我们几乎总是用 const 来声明响应式状态,因为:
reactive()返回的对象引用不应该被替换(替换会丢失响应性)
ref()返回的包裹对象引用也不应该被替换(修改值应该通过 .value)
举例如下:
js
// 用 const,之后通过 .value 或 .属性 修改内部值
const message = ref('Hello')
message.value = 'World' // 改的是内部值,不是 message 这个变量本身
// 用 let 然后整体替换,会导致模板失去响应性
let message = ref('Hello')
message = ref('World') // 模板绑定的还是旧的 ref
然后reactive里的花括号即键值对:
js
{ count: 0 }
// ↑ key(键): value(值)
//等价于
const obj = new Object()
obj.count = 0
reactive({ count: 0 }) 的意思就是:把一个包含 count 属性的普通对象,交给 Vue 变成响应式的。我们可以放任意多个键值对进去:
js
const state = reactive({
name: 'Shorekiten', // string
age: 0, // number
hobbies: ['coding'], // array
isOnline: true // boolean
})
那么最后的counter.count就是访问响应式对象的属性
知道了这些我们就能创造自己的页面内容了:
js
<script setup>
import { reactive, ref } from 'vue'
const state = reactive({
name: 'Shorekiten', // string
age: 0, // number
hobbies: ['coding'], // array
isOnline: true // boolean
})
const message = ref('ShoreKiten の cottage')
const title = ref('123')
</script>
<template>
<h1>{{ message }}</h1>
<p>State is: {{ state.isOnline}}</p>
<h2>{{ title }}</h2>
<p>This is second paragraph</p>
</template>

然后在写的过程中其实遇到了一点问题,首先我刚开始是这么写的:
js
JS 认为字符串在 ShoreKiten 后就结束了
const message = ref('ShoreKiten's cottage')
解决方法有三种:
js
//用反引号,里面可以随意包含单引号和双引号,无需转义:
const message = ref(`ShoreKiten's cottage`)
//使用双引号包裹
const message = ref("ShoreKiten's cottage")
//使用转义字符 \
const message = ref('ShoreKiten\'s cottage')
还有一个是之前提到的.value的问题,在模板中是不需要加的,但是在模板外可以加上这个对原本的内容进行相应的修改,就比如:
js
const message = ref(`ShoreKiten's cottage`)
message.value = `Tom's house`
那么最终结果就是这样显示:

Attribute 绑定
这个在快速入门里面讲的不是很详细,然后参考了网上别的师傅写的文章,首先得是理解这个标题是什么意思:
属性绑定**(Attribute Binding)**是 Vue 提供的一种将组件数据与 HTML 元素属性建立关联的方式。通过属性绑定,我们可以让元素的属性值随着数据的变化而自动更新,实现数据驱动视图的核心思想,基础语法如下:
js
<元素 v-bind:属性名="数据"></元素>
//一般进行简写,即快速入门中描述的
<div :id="dynamicId"></div>
常见使用场景
动态图片/链接
js
<script setup>
import { ref } from 'vue'
const avatarUrl = ref('/images/shorekiten.png')
const blogLink = ref('https://www.cnblogs.com/ShoreKiten')
</script>
<template>
<img :src="avatarUrl" alt="头像" />
<a :href="blogLink">访问博客</a>
</template>

动态控制 disabled / readonly 等布尔属性
js
<script setup>
import { ref } from 'vue'
const isSubmitting = ref(false)
</script>
<template>
<!-- isSubmitting 为 true 时按钮禁用,为 false 时恢复正常 -->
<button :disabled="isSubmitting">提交</button>
</template>

class 和 style
这里其实更常用的是数组,对象等等,但是因为是初学所以就先不拓展了,样例给出的结果:
js
<script setup>
import { ref } from 'vue'
// 1. 声明一个响应式变量,值为字符串 'title'
const titleClass = ref('title')
</script>
<template>
<!-- 2. 用 :class 把 h1 的 class 属性与 titleClass 变量绑定 -->
<h1 :class="titleClass">Make me red</h1>
</template>
Attribute 绑定 vs 文本插值 {{ }} 的区别
| 特性 | {{ }} 文本插值 |
:attr 属性绑定 |
|---|---|---|
| 用在哪里 | 标签内部的文本内容 | 标签上的 HTML 属性 |
| 示例 | <p>{{ message }}</p> |
<img :src="url" /> |
| 能否混用 | ❌ 不能写在属性里 | ❌ 不能写在文本位置 |
事件监听
就类似于HTML DOM事件,在快速入门中给出的案例就是点击按钮计数增加:
js
<button v-on:click="increment">{{ count }}</button>
//简写
<button @click="increment">{{ count }}</button>
快速入门中在文本中已经给出了相应答案,我们可以在此基础上添加相应变种:
js
<script setup>
import { ref } from 'vue'
const count1 = ref(0)
const count2 = ref(0)
function increase(){
count1.value++
}
function decrease(){
count2.value--
}
</script>
<template>
<!-- 使此按钮生效 -->
<button @click="increase">Count1 is: {{ count1 }}</button>
<h2>
<button @click="decrease">Count2 is:{{ count2 }}</button>
</h2>
</template>

或者更换任意鼠标事件:
| Vue 写法 | 对应原生事件 | 触发时机 |
|---|---|---|
@mouseenter |
onmouseenter |
鼠标进入元素边界(不冒泡) |
@mouseleave |
onmouseleave |
鼠标离开元素边界(不冒泡) |
@mouseover |
onmouseover |
鼠标移入元素(会冒泡) |
@mouseout |
onmouseout |
鼠标移出元素(会冒泡) |
@mousemove |
onmousemove |
鼠标在元素内移动(高频触发⚠️) |
@mousedown |
onmousedown |
鼠标按键按下 |
@mouseup |
onmouseup |
鼠标按键抬起 |
@dblclick |
ondblclick |
双击 |
@contextmenu |
oncontextmenu |
右键菜单 |
js
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increase() {
count.value++
}
function decrease() {
count.value--
}
</script>
<template>
<!-- 鼠标移入 +1,移出 -1 -->
<button @mouseenter="increase" @mouseleave="decrease">
Count is: {{ count }}
</button>
</template>
表单绑定
表单绑定指的是:将表单元素 (<input>、<select>、<textarea> 等)的值与 Vue 的响应式数据进行双向同步。
简单来说就是:
数据 → 视图:JS 变量变了,输入框里的内容自动更新
视图 → 数据:用户在输入框里打字/选择,JS 变量自动更新
js
<script setup>
import { ref } from 'vue'
const message = ref('hello')
</script>
<template>
<!-- 推荐写法 -->
<input v-model="message" />
<!-- 等价于上面这行(手动实现双向绑定) -->
<input :value="message" @input="message = $event.target.value" />
<p>你输入了:{{ message }}</p>
</template>

条件渲染
条件渲染指的是:根据 JavaScript 表达式的真假值,动态地决定某个 DOM 元素是否显示、或者显示哪一个元素,简单来说就是:让模板具备"判断能力",而不是永远写死所有 UI
v-if:真正的条件渲染
条件为 false 时,元素直接从 DOM 中移除/销毁;为 true 时才创建并插入 DOM
适用场景:切换频率低、条件很少改变的场景(如权限控制、弹窗)

就拿文档里面的来说要调用toggle来改变awesome的真值,在 awesome 的值为真时渲染。若 awesome 更改为假,它将被从 DOM 中移除,文档中因此给出了vi-if和vi-else两个条件判断在awesome的值为真或假时的两种显示,我们需要做的就是补全toggle函数,让调用函数的按钮在被点击时能够改变awesome的真假,这里就可以直接运用:
js
function toggle() {
awesome.value = !awesome.value
}
或者手动进行更改
js
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>
<template>
<!-- isLoggedIn 为 false 时,这个 button 完全不存在于 DOM 中 -->
<button v-if="isLoggedIn">退出登录</button>
<button v-else>请登录</button>
</template>
然后这里也有多分支判断,就比如:
js
<script setup>
import { ref } from 'vue'
const awesome = ref(100)
function toggle() {
awesome.value -=10
}
</script>
<template>
<input v-model= "awesome" />
<button @click="toggle">Toggle</button>
<h1 v-if="awesome>=80">Vue is awesome!</h1>
<h1 v-else-if="awesome>=60">Vue is normal </h1>
<h1 v-else>Opus! </h1>
</template>
这里就是三个条件判断,初始的值为100,每次点击按钮之后-10,最终呈现的是三种不同的效果,然后再来一个表单绑定实时显示awesome的大小,这样看的就更加清楚了。

列表渲染
列表渲染指的是:使用 v-for 指令,根据数组或对象的数据,自动生成一组结构相同的 DOM 元素。
简单来说就是:我们只需要写一次模板代码,Vue 会根据数据的长度自动"复制"出 N 个元素。它是条件渲染(控制单个元素的有无)的自然延伸------从"控制一个"变成"批量生成多个"。
文档中直接给出的例子有点复杂,先从一个简单的看起:
js
<script setup>
import { ref } from 'vue'
const fruits = ref(['苹果', '香蕉', '橘子'])
</script>
<template>
<!-- item 是每一项的值,index 是索引 -->
<ul>
<li v-for="(item, index) in fruits" :key="index">
{{ index + 1 }}. {{ item }}
</li>
</ul>
</template>

这里并不是直接的todo列表,而是通过数组下标来进行创建相应的元素,当我们插入或者删除其中某个元素时,后续所有key的index都会发生改变,那么就要回到样例中的todo列表了,但这里还是拿水果来举例,改成下面这样子:
js
<script setup>
import { ref } from 'vue'
let id = 0
const newFruit = ref('')
const fruits = ref([
{ id: id++, name: '苹果' },
{ id: id++, name: '香蕉' },
{ id: id++, name: '橘子' }
])
function addFruit() {
fruits.value.push({ id: id++, name: newFruit.value })
newFruit.value = ''
}
function removeFruit(fruit) {
fruits.value = fruits.value.filter((f) => f !== fruit)
}
</script>
<template>
<form @submit.prevent="addFruit">
<input v-model="newFruit" required placeholder="添加水果">
<button>添加</button>
</form>
<ul>
<li v-for="fruit in fruits" :key="fruit.id">
{{ fruit.name }}
<button @click="removeFruit(fruit)">X</button>
</li>
</ul>
</template>
这里关键是用的两个方法:
js
// 【添加方法】由表单 @submit.prevent 触发
function addTodo() {
// 1. 向响应式数组末尾追加新对象
// Proxy 拦截 push → 自动通知依赖 fruits 的 v-for 重新渲染
// id++ 先取当前值赋给 id,再自增,确保不重复
fruits.value.push({ id: id++, text: newFruit.value })
// 2. 清空输入框(v-model 同步更新 DOM 中 input 的值)
newFruit.value = ''
}
js
fruits.value = fruits.value.filter((f) => f !== fruit)
// ↑ ↑
// 遍历的每一项 要删除的目标项
filter 会遍历数组中的每一项,对每一项执行回调函数 (f) => f !== fruit:
当 f 是要删除的那个水果时 →f !== fruit 为 false被剔除
当 f 是其他水果时 → f !== fruit 为 true 被保留
最终返回一个全新的数组,里面不包含被点击删除的那一项。
然后!== 是严格不等于,比较的是对象引用(内存地址),而不是值的内容
最后是将新得到的数组重新进行赋值,原因是filter 不会修改原数组,它返回的是一个新数组。必须将新数组赋值回 fruits.value,才能触发 Vue 的响应式更新
计算属性
生命周期和模板引用
生命周期
生命周期就是 Vue 组件从 创建 → 挂载 → 更新 → 销毁 的完整过程。Vue 在特定阶段会自动调用对应的钩子函数,让我们能在"正确的时机"执行代码
核心钩子一览:
| 钩子 | 触发时机 | 典型用途 |
|---|---|---|
onBeforeMount |
DOM 还没生成 | 极少使用 |
onMounted |
DOM 已渲染到页面 | 获取 DOM 尺寸、初始化第三方库、发请求、自动聚焦输入框 |
onBeforeUpdate |
数据变了,DOM 还没更新 | 记录更新前的滚动位置等 |
onUpdated |
DOM 已同步最新数据 | 操作更新后的 DOM(慎用,易死循环) |
onBeforeUnmount |
组件即将被移除 | 清除定时器、取消订阅、断开 WebSocket |
onUnmounted |
组件已从 DOM 移除 | 最终清理 |
模板引用
板引用是通过 ref() + 同名属性,在 <script setup> 中直接获取某个 DOM 元素或子组件实例的桥梁。它是 Vue 对原生 document.getElementById() 的响应式替代方案
二者结合
拿官方文档中的例子来看:
js
<script setup>
import { ref, onMounted } from 'vue'
const pElementRef = ref(null)
onMounted(() => {
pElementRef.value.textContent = 'mounted!'
})
</script>
<template>
<p ref="pElementRef">Hello</p>
</template>
首先是声明模板引用:
js
const pElementRef = ref(null)
ref(null):创建一个响应式变量,初始值为 null。
null是因为此时组件还没有渲染,DOM 还不存在,所以暂时没有东西可以绑定。
命名约定:变量名 pElementRef 必须与下面模板中ref="pElementRef" 完全一致,Vue 靠这个名字建立关联
然后在模板中进行相应绑定
js
<p ref="pElementRef">Hello</p>
Vue 渲染到这个 <p> 标签时,发现 ref="pElementRef"
Vue 自动将真实的 <p> DOM 节点赋值给 pElementRef.value
这个赋值动作发生在 DOM 插入页面之后、onMounted 回调执行之前
最后在生命周期中安全访问:
js
onMounted(() => {
pElementRef.value.textContent = 'mounted!'
})
onMounted:确保回调在组件挂载完成后才执行。此时 pElementRef.value 已经不再是 null,而是真实的 <p> 元素。
.textContent = 'mounted!':直接调用原生 DOM API,将段落文本从 "Hello" 修改为 "mounted!"
结果:用户在页面上看到的是 mounted! 而不是 Hello
js
① <script setup> 顶层代码执行
→ pElementRef = ref(null) // 值是 null
② 模板渲染,DOM 插入页面
→ pElementRef.value = <p>元素 // Vue 自动赋值
③ onMounted 回调执行
→ pElementRef.value.textContent = 'mounted!' // 安全访问
侦听器
这里描述的比较详细,就不再多说了:

组件
嵌套组件概念的引入:
父组件可以在模板中渲染另一个组件作为子组件。要使用子组件,我们需要先导入它,然后就能在模板中使用组件:

Props
也类似于cpp中的继承,描述得也比较详细,注意原文档中给出的是动态传递,如果不加冒号的话便是静态,直接输出后面的字符串:


目前看的也就这些,作为一个初步的了解