Vue 3 语法要点

要点

七组语法,每组记一条”为什么”:默认 ref(读写踪迹明确)、派生用 computed(缓存)、副作用三选一(watch 显式、watchEffect 自动)、props 只读 emits 通知(单向数据流)、v-model 是语法糖、插槽把内容的决定权还给调用方、组合式函数是逻辑复用单位。

一、ref 与 reactive:为什么默认 ref

两个都能造响应式状态,官方推荐默认 ref。先看 reactive 的三个失联时刻:

import { reactive } from 'vue'

let form = reactive({ name: 'ada', tags: ['vue'] })
const { name } = form      // 解构:name 是普通字符串,从此与 form 无关
form = reactive({ name: 'bob' })  // 替换引用:旧代理失联,界面对不上新对象
logIt(form.name)           // 传出的是值的拷贝,收到方改它不影响 form

reactive 返回代理对象(Proxy),响应性挂在这个代理上;解构、整体替换、按值传参都会丢掉代理。ref 没有 these 陷阱,代价是 script 里要写 .value:

维度 ref reactive
支持的类型 任意(数字、布尔、字符串……) 仅对象与集合
整体替换 list.value = newList 安全 替换引用即失联,只能逐属性改
解构/传参 ref 本身就是引用,不解体 解构即丢失响应性
书写成本 script 里处处 .value 少敲几个字符

默认 ref 的理由汇总:.value 让”这一行在改状态”在代码里显式可见;整表替换安全;组合式函数返回值约定统一(见第七节)。reactive 留给”一组字段的表单”这类天然聚合、且不会整体替换的对象。

二、computed、watch、watchEffect:派生与副作用

const query = ref('vue')

// computed:派生值。依赖不变就用缓存,不重算
const results = computed(() => searchAll(query.value))

// watch:显式点名要盯谁。默认惰性(首次不执行),回调拿得到新旧值
watch(query, (next, prev) => saveHistory(next, prev))

// watchEffect:立即执行一次,执行中读到的响应式数据自动成为依赖
watchEffect(() => { document.title = `搜索:${query.value}` })

为什么派生值用 computed 而不是方法调用:模板每次重渲染都会调方法,computed 缓存后依赖不变直接返回旧值——列表过滤、合计这类计算,差距会在数据量上来后显现。

API 首次何时跑 依赖怎么来 典型用途
computed 首次被读取时 getter 里自动收集 派生状态(过滤、合计、格式化)
watch 默认惰性,变化才跑 第一个参数显式声明 要新旧值对比、异步请求、防抖
watchEffect 立即执行一次 回调执行时自动收集 把状态同步到外部(标题、canvas、第三方库)

三、props 与 emits:单向数据流

<!-- Item.vue:defineProps/defineEmits 是编译器宏,无需 import -->
<script setup>
defineProps({ item: { type: Object, required: true } })
const emit = defineEmits(['remove'])
</script>
<template>
  <li @click="emit('remove', item.id)">{{ item.text }}</li>
</template>

为什么子组件不许改 props:数据属于父组件,”谁声明谁修改”,出问题时”这行代码谁改的”才有唯一答案。子组件想改,就 emit 事件让主人动手——链路多一步,换来的是调试时数据流向永远可追溯。反模式:子组件里给 props 对象的属性赋值,界面可能”碰巧”更新(引用共享),但 Vue 会警告,且父组件下一次渲染就把你的修改覆盖回去。

四、v-model:双向绑定的语法糖本质

<input v-model="text" />
<!-- 完全等价于: -->
<input :value="text" @input="text = $event.target.value" />

记住糖的配方,”双向不生效”才能拆开排查。自定义组件接 v-model,Vue 3.4 起用 defineModel 宏:

<!-- SearchBox.vue -->
<script setup>
const model = defineModel()   // 声明后组件即可被父级 v-model
function clear() { model.value = '' }
</script>
<template><input v-model="model" /><button @click="clear">清空</button></template>

本质仍是:父传 modelValue、子 emit update:modelValue。糖可以吃,配方必须背。

五、插槽:内容分发

<!-- Card.vue:布局归组件,内容归调用方 -->
<template>
  <div class="card">
    <header><slot name="title">默认标题</slot></header>
    <main><slot /></main>
  </div>
</template>

<!-- 调用:内容在调用方写,结构由组件定 -->
<Card><template #title>周报</template>本周完成了三件事…</Card>

作用域插槽(scoped slot)反向把数据递出来,列表组件最常用:

<!-- List.vue -->
<li v-for="t in todos"><slot :item="t">{{ t.text }}</slot></li>
<!-- 调用方拿到 item 自行决定渲染成什么 -->
<List v-slot="{ item }"><b :class="{ done: item.done }">{{ item.text }}</b></List>

为什么用插槽而不是 props 传渲染函数:插槽是声明式的模板语法,作用域插槽让”数据在子、模板在父”不必靠函数传来传去。

六、生命周期:多数时候只需要两个钩子

setup 执行点在 beforeCreate 与 created 之间,所以 script setup 里根本不需要这两个钩子。真正常用的是:

onMounted(() => {       // DOM 就绪:发首个请求、初始化图表、聚焦输入框
  inputEl.value?.focus()
})
onUnmounted(() => clearInterval(timer))   // 与注册成对出现,防泄漏
钩子 时机 常用度
onBeforeMount / onMounted 挂载前 / DOM 就绪 onMounted 高频
onBeforeUpdate / onUpdated 更新前 / 更新后 低——想对变化做反应,watch 通常更合适
onBeforeUnmount / onUnmounted 卸载前 / 卸载后 onUnmounted 高频,清理副作用

七、组合式函数:逻辑复用单位

// useCounter.js:一段"状态 + 操作"的打包,use 前缀是社区约定
import { ref } from 'vue'
export function useCounter(initial = 0) {
  const count = ref(initial)
  const inc = () => count.value++
  return { count, inc }   // 返回 ref 而非解构值,响应性才带得出去
}
// 使用方解构安全:每个字段都是独立 ref
const { count, inc } = useCounter(10)

为什么不用 Vue 2 时代的 mixins:mixin 的字段来源不可见、命名冲突静默覆盖;组合式函数的来源就是 import 语句,类型可推导。与 React hooks 的差别在执行次数:React hooks 每次渲染重跑、依赖调用顺序;Vue 的 setup 只跑一次,没有”不能在 if 里调用”这类规则——但这不等于没有规则,依赖收集仍是 Vue 的隐性契约。两套模型的来龙去脉见《React Hooks 心智模型》,React 为何选择整组件重跑见《React 渲染与重渲染》。

最佳实践

状态声明默认 ref,reactive 只在”聚合对象且不整体替换”时用;派生优先 computed,副作用按”要不要新旧值、要不要立即执行”在 watch 与 watchEffect 里选;组件边界靠 props/emits 与插槽表达,跨层级共享再考虑 provide/inject 或 Pinia。

常见问题

Q1: reactive 解构后界面不更新了

解构拿到的已不是代理。局部修复用 toRefs(form) 把每个字段转成 ref 再解构;新代码直接改用 ref 声明各字段更省心。

Q2: watch 侦听 ref 对象的嵌套属性不触发

ref 包对象时 watch 默认浅侦听,只盯 .value 的引用替换。要么传 getter 指名道姓 watch(() => form.value.name, cb),要么 { deep: true } 整树盯防。

练习与实践

练习一:把第一个应用里的”剩余计数”改成 watch 实现,再改回 computed,体感两者的分工。练习二:抽一个 useLocalStorage(key) 组合式函数,读写待办列表。评估标准:能说出每处选择”为什么不是另外两个 API”。

延伸阅读

逐条查签名、找坑,看下一篇《Vue 3 基础知识参考字典》;工程与调试回到系列首篇《Vue 3 环境搭建与工具链》。

版本核实(撰写时检索,2025-03):本文 API 均为 Vue 3 稳定特性;defineModel 于 Vue 3.4 转稳定;Vue 当前稳定线 3.5.x,3.6(含 Vapor Mode)仍在实验、未发布。具体以官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇