Vue3 API 易漏点

:(布尔型attribute)="",真值及''代表开启

<button :disabled="isButtonDisabled">Button</button>

''/true/1/'hello'/'false' 均为真值, 渲染为 disabled

false/null/undefined 为假值, 不渲染 disabled

:(无参)="对象类型",该对象所有子属性 被绑定为attribute

对象的每个键值对会逐一展开为元素的 attribute, 等价于手动写 :id="..." :class="..." :style="..."

<script setup>
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper',
  style: 'background-color:green',
}
</script>

<template>
  <div :="objectOfAttrs">hi</div>
</template>

渲染结果:

<div id="container" class="wrapper" style="background-color: green;">hi</div>

{{}}v-xx指令 的值中 均可使用 JS表达式

JS表达式 检验标准为 可以合法的被表达在 return 后面

包括函数调用(该函数可以但不应该内含副作用)

但下面的例子都是无效的:

<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}
<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}

模板中的 JS表达式 仅能够访问到有限的常用的全局对象列表

Math/Date 等可以访问

用户附加在window上的属性 等没有显式包含在列表中的全局对象不能访问

通过 app.config.globalProperties 可以进行配置

v-xx内置指令及其参数意义

必须无参数

只接表达式,不接 :arg

指令 用途
v-text 设置 textContent
v-html 设置 innerHTML
v-show 切换 display
v-if / v-else-if / v-else 条件渲染(v-else 连表达式都不要)
v-for 列表渲染
v-pre 跳过该元素及子元素的编译
v-once 只渲染一次
v-memo 依赖数组未变则跳过更新
v-cloak 编译完成前的占位钩子(CSS 配合)

参数可选(无参时绑定对象 / 默认名)

指令 有参 无参语义
v-bind: :id="x" 绑定单个 attr v-bind="{id, class}" 展开对象
v-on@ @click="fn" 绑定单事件 v-on="{click: fn, blur: fn2}" 对象批量绑定
v-model v-model:title="x"(组件多 prop) 默认绑定 modelValue(组件)或对应表单值
v-slot# #header 指定具名插槽 默认 default 插槽

:[动态参数]="xxx"

动态参数 必须为 string 或 特殊值null(移除该绑定)

<!-- 动态参数 []内不允许含''和空格,会触发编译器警告 -->
<a :['foo' + bar]="value"> ... </a>
<!-- DOM 内嵌模板下 动态参数 需要避免在名称中使用大写字母 -->
<a :[someAttr]="value"> ... </a>

<!-- DOM 内嵌模板:   直接写在 .html 里、由 #app 这种挂载点拿到的模板 -->
<!-- SFC(.vue 文件): 模板由 Vue 的编译器解析 -->

await nextTick(), DOM 更新完成后再执行

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

ref 是带有响应式的引用传递, 使得 Composable 模式成为可能

Composable 模式

Composable 模式 = 把组件里"一段带响应式状态 + 生命周期 + 副作用"的逻辑,抽到一个普通 JS 函数里,让多个组件 import 后复用。

是 Vue 3 Composition API 提供的官方逻辑复用方案,取代 Vue 2 的 mixin, 参考 VueUse库。

why not 普通工具函数? 其没法复用"带状态、带生命周期、和组件响应式系统连着的逻辑"
why not mixin? 其 直接黑盒注入this

ref = 引用(跨函数传递不丢失状态) + 响应式(读写会被 track/trigger)

两者都成立时 都成立时,composable 模式才能跑通。

Composition API 与 ref/reactive

Vue 2 响应式只能挂在组件实例 this 上,所以复用带状态的逻辑只能靠 mixin 黑盒注入

Vue 3 用 Composition API 让响应式脱离实例独立存在,

配合 ref/reactive 可传递的响应式容器,就能把逻辑抽象成普通函数(composable)

显式return、显式接收,可读性、可组合性、类型推导都比 mixin 好

推荐使用 ref() 而非 reactive(仅限对象)

ref(对象)reactive 多套一层 .value 壳,内层仍是 reactive

ref(obj)       // 返回 { value: reactive(obj) }
reactive(obj)  // 返回 Proxy(obj)

reactive() 缺点: 只能用于对象类型, 替换整个对象丢失响应性, 解构出的属性丢失响应性

ref() 缺点: 必须通过.value访问(模板里不需要), 解构出的属性丢失响应性

维度 ref(对象) reactive(对象)
访问语法(JS) a.value.n b.n
模板访问 自动解包 {{ a.n }} {{ b.n }}
整体替换 a.value = {...} b = {...} ❌(丢失响应性)
解构是否丢响应 .value 解构会丢,需用 toRefs 或保留 .value 直接解构会丢,需 toRefs(b)
容器类型 原始类型 / 对象 / Map / Set 全支持 仅对象类引用类型

壳的价值在于「可整体替换、可解构后保留响应、能装原始值」,代价是写 .value

toRef() 解构不丢失响应性,本质是代理到原reactive上

import { reactive, ref, toRefs } from 'vue'

// reactive
const state = reactive({ name: 'a', age: 1 })
const { name, age } = toRefs(state)
name.value = 'b'        // ✅ 改 name.value 等于改 state.name

// ref(对象) —— 对 .value 用 toRefs
const userRef = ref({ name: 'a', age: 1 })
const { name: n, age: g } = toRefs(userRef.value)
n.value = 'b'           // ✅ 同步到 userRef.value.name

// toRef 另一个用法
const ageRef = toRef(state, 'age')
ageRef.value++          // ✅ 等于 state.age++

ref 自动解包(不用写.value)

下面两种情况会自动解包

1. 模板中使用的, setup 暴露的顶层 ref

<script setup>
const count = ref(0)
const user = ref({ name: 'foo' })
const object = { count: ref(0) }
const list = [ref(0)]
</script>

<template>
  {{ count }}          <!-- ✅ 自动解包 -->
  {{ user.name }}      <!-- ✅ -->
  {{ count + 1 }}      <!-- ✅ -->
  {{ object.count }}     <!-- ❌ object 是普通对象、count 是 ref 时不解包 -->
  {{ list[0] }}          <!-- ❌ 数组索引访问不解包 -->
</template>

绕过方法: 先在JS中解构出顶层引用 const { count } = object

2. 模板 或 js中使用的, reactive 对象的属性的 ref

const count = ref(0)
const state = reactive({ count })

state.count       // ✅ 0(自动解包,不是 ref 对象)
state.count++     // ✅ 等价于 count.value++

注意:
1.state.count = 非proxy 原始类型 值, count 也会变化, 两者指向同一份数据。(上例)
2.state.count = (proxy或)引用类型 值, count 作为旧ref,依赖解除,关联新ref。(下例)

const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1

shallowReactive / shallowRef 嵌套 ref 不解包

只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

const count = ref(0)
const obj = shallowReactive({ c: count })
obj.c          // ❌ ref 对象,需 obj.c.value
const state = shallowRef({ c: count })
state.value.c  // ❌ ref 对象,需 state.value.c.value

什么时候用 shallow?
大型不可变数据: 第三方库实例、大 JSON、Three.js 场景对象 —— 深响应代价高且无意义
手动控制更新时机:用 triggerRef(shallowRef) 强制触发
集成外部状态库: 状态对象内部由库管理,只需要"指针变了"这一层信号

reactive / ref 重新赋值 → 响应性丢失

响应式追踪发生在 属性访问 层面(proxy.count 触发 Proxy 的 get/set)。变量本身只是 JS 绑定,重新赋值只改变绑定指向,Vue 无从感知。

// reactive
let state = reactive({ count: 0 })
state = reactive({ count: 1 })   // ❌ 断链:旧依赖仍追踪旧 Proxy
state = { count: 1 }             // ❌ 更糟:新对象彻底失去响应性

// ref
let state = ref(0)
state = ref(1)        // ❌ 断链(替换了整个 ref 对象)
state.value = 1       // ✅ 触发 ref 的 setter,依赖正常通知

整体替换推荐方式

容器 安全写法 原因
reactive Object.assign(state, {...}) 改的是原 Proxy 的属性
ref(对象) state.value = {...} 触发 ref 的 setter
reactive 想整体替换 改用 ref(对象) 包一层 通过 .value 替换

核心规则:reactive 返回的 Proxy 一旦创建就不能让变量再指向别的对象,只能修改它的属性;这也是官方推荐 ref 优先于 reactive 的主要原因之一。

v-ifv-for 不应同元素并用,二者优先级不明显

两种典型场景及替代写法:

场景 反例 推荐
过滤列表项 v-for="user in users" v-if="user.isActive" computed 派生 activeUsers,再 v-for
整体显隐列表 v-for="user in users" v-if="shouldShowUsers" v-if 提到容器(<ul>/<ol>)上

computed 返回的也是 ref, 解包规则同 ref, 避免副作用

computed 与 函数 的区别在于,计算属性存在缓存, 自动收集依赖,依赖未变,则直接使用缓存

作为派生值, 过滤数组(代替模板内for循环嵌套if), 创建classObject

// computed 返回 classObject 是一个常见技巧
const isActive = ref(true)
const error = ref(null)

const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>

注意 要避免副作用

computed 声明中描述的是如何根据其他值派生一个值, 本质是派生状态,因而
computed 或者说其 getter 不该含有副作用, 且 不该直接修改 computed值, 保证数据自上而下

// 尤其注意数组中改变原数组的七个函数
- return numbers.reverse()
- return [...numbers].reverse()