Vue 3 与现代 JavaScript 常用语法指南

Vue 3 与现代 JavaScript 常用语法指南

面向刚接触前端、准备阅读和维护 Vue 3 项目的开发者。

本文示例默认使用 Vue 3、<script setup> 和 TypeScript。阅读顺序建议:JavaScript 基础 -> Vue 模板 -> 响应式 -> 组件通信 -> 实战示例。

快速导航

  • JavaScript 基础:第 1 至 10 节,重点掌握变量、函数、对象、数组、模块和异步。
  • Vue 模板语法:第 11 至 12 节,重点掌握 :@v-ifv-forv-model
  • Vue 组件开发:第 13 至 22 节,重点掌握响应式、组件通信、生命周期和 TypeScript。
  • 实战与复习:第 23 至 28 节,包含完整示例、项目写法、易错点和速查表。

1. 先理解"语法糖"

语法糖不是新能力,而是同一种能力的简便写法。掌握简写的同时,也要知道它展开后是什么。

js 复制代码
// 普通写法
const verboseUser = {
  name: name,
  sayHello: function () {
    console.log('hello')
  },
}

// ES6 语法糖
const conciseUser = {
  name,
  sayHello() {
    console.log('hello')
  },
}

Vue 中也有大量语法糖:

vue 复制代码
<!-- 完整写法 -->
<button v-bind:disabled="loading" v-on:click="submit">提交</button>

<!-- 常用简写 -->
<button :disabled="loading" @click="submit">提交</button>

2. JavaScript、ES6 与 TypeScript 的关系

名称 作用 常见内容
JavaScript 浏览器真正执行的编程语言 变量、函数、对象、数组、异步任务
ES6 / ES2015 2015 年发布的一次重要 JavaScript 标准 letconst、箭头函数、解构、Promise、模块化
现代 JavaScript ES6 以及后续版本的常用语法统称 async/await、可选链、空值合并等
TypeScript 在 JavaScript 上增加类型检查 interfacetype、泛型、参数和返回值类型
Vue 用状态驱动页面的前端框架 模板、响应式、组件、生命周期
Nuxt 基于 Vue 的应用框架 文件路由、布局、自动导入、构建

注意:async/await、可选链 ?. 和空值合并 ?? 严格来说不是 ES6,但现代 Vue 项目会高频使用,本文一并介绍。

3. 变量:constletvar

3.1 优先使用 const

js 复制代码
const name = '张三'
let age = 18

age = 19
  • 默认使用 const
  • 确实需要重新赋值时使用 let
  • 新代码不要使用 var,它存在函数作用域和变量提升等容易混淆的问题。

const 表示变量不能重新指向其他值,不表示对象内部不能修改:

js 复制代码
const user = { name: '张三' }

user.name = '李四' // 可以
// user = { name: '王五' } // 报错:不能重新赋值

4. 字符串与模板字符串

反引号可以在字符串中插入变量,也可以直接换行:

js 复制代码
const name = '张三'
const age = 18

const text = `姓名:${name},年龄:${age}`
const message = `第一行
第二行`

传统拼接与模板字符串对比:

js 复制代码
const oldText = '姓名:' + name + ',年龄:' + age
const newText = `姓名:${name},年龄:${age}`

${} 内可以写表达式:

js 复制代码
const price = 10
const count = 3
const result = `合计:${price * count} 元`

5. 函数常用语法

5.1 函数声明

项目业务方法推荐使用函数声明,名称清晰,并且不会受声明位置影响:

ts 复制代码
/**
 * 计算订单总价。
 *
 * @param price 商品单价
 * @param count 商品数量
 * @returns 订单总价
 */
function calculateTotal(price: number, count: number): number {
  return price * count
}

5.2 箭头函数

箭头函数适合短回调:

js 复制代码
const numbers = [1, 2, 3]
const doubled = numbers.map(item => item * 2)

以下写法等价:

js 复制代码
const verboseAdd = (a, b) => {
  return a + b
}

const conciseAdd = (a, b) => a + b

只有一个参数时可以省略括号:

js 复制代码
const names = users.map(user => user.name)

注意:箭头函数没有自己的 this。对象方法、需要动态 this 的函数不要盲目改成箭头函数。

5.3 默认参数

js 复制代码
function greet(name = '游客') {
  return `你好,${name}`
}

greet() // 你好,游客
greet('张三') // 你好,张三

只有参数为 undefined 或未传递时才会使用默认值;传入 null 不会触发默认值。

5.4 剩余参数 ...

js 复制代码
function sum(...numbers) {
  return numbers.reduce((total, number) => total + number, 0)
}

sum(1, 2, 3) // 6

这里的 ...numbers 表示"把剩余参数收集成数组"。

6. 对象常用语法

6.1 属性简写与方法简写

js 复制代码
const name = '张三'
const age = 18

const user = {
  name,
  age,
  greet() {
    return `你好,我是${this.name}`
  },
}

等价于:

js 复制代码
const user = {
  name: name,
  age: age,
  greet: function () {
    return `你好,我是${this.name}`
  },
}

6.2 对象解构

js 复制代码
const user = {
  id: 1,
  name: '张三',
  age: 18,
}

const { name, age } = user

重命名、默认值和嵌套解构:

js 复制代码
const { name: userName, gender = '未知' } = user

const response = {
  data: {
    list: [1, 2, 3],
  },
}

const {
  data: { list },
} = response

函数参数也可以解构:

ts 复制代码
interface User {
  id: number
  name: string
}

function printUser({ id, name }: User) {
  console.log(id, name)
}

6.3 对象展开 ...

js 复制代码
const user = { id: 1, name: '张三' }
const updatedUser = {
  ...user,
  name: '李四',
}

后面的同名属性会覆盖前面的属性。展开是浅拷贝,嵌套对象仍然共享引用:

js 复制代码
const source = {
  profile: { city: '北京' },
}
const copied = { ...source }

copied.profile.city = '上海'
console.log(source.profile.city) // 上海

6.4 可选链 ?.

js 复制代码
const city = user.profile?.address?.city
const firstName = users?.[0]?.name
const result = callback?.()

如果 ?. 左侧是 nullundefined,表达式直接返回 undefined,不会继续访问并报错。

6.5 空值合并 ??

js 复制代码
const pageSize = config.pageSize ?? 20

只有左侧为 nullundefined 时才使用右侧默认值。

js 复制代码
0 || 20 // 20
0 ?? 20 // 0

'' || '默认文字' // 默认文字
'' ?? '默认文字' // 空字符串

0false、空字符串是有效业务值时,应该使用 ??,不要使用 || 设置默认值。

7. 数组常用语法与方法

7.1 数组解构

js 复制代码
const colors = ['red', 'green', 'blue']
const [first, second] = colors

const [head, ...rest] = colors

交换变量:

js 复制代码
let a = 1
let b = 2

;[a, b] = [b, a]

开头的分号用于避免这一行被错误地连接到上一条语句。

7.2 数组展开

js 复制代码
const oldList = [1, 2]
const newList = [...oldList, 3]
const merged = [...oldList, ...newList]

7.3 高频数组方法

js 复制代码
const users = [
  { id: 1, name: '张三', enabled: true },
  { id: 2, name: '李四', enabled: false },
]

// 转换每一项,返回新数组
const names = users.map(user => user.name)

// 过滤,返回符合条件的新数组
const enabledUsers = users.filter(user => user.enabled)

// 找到第一项,找不到返回 undefined
const user = users.find(user => user.id === 2)

// 找到第一项的下标,找不到返回 -1
const index = users.findIndex(user => user.id === 2)

// 是否至少有一项满足条件
const hasEnabledUser = users.some(user => user.enabled)

// 是否每一项都满足条件
const allEnabled = users.every(user => user.enabled)

// 遍历,不返回有业务意义的新数组
users.forEach(user => console.log(user.name))

汇总计算使用 reduce

js 复制代码
const orders = [
  { amount: 10 },
  { amount: 20 },
]

const total = orders.reduce((sum, order) => sum + order.amount, 0)

判断方法是否修改原数组:

不修改原数组 会修改原数组
mapfiltersliceconcat pushpopshiftunshift
findsomeeveryreduce splicesortreverse

Vue 响应式数组支持两类方法,但维护复杂状态时,返回新数组的写法通常更容易推理。

8. 判断、短路运算与三元表达式

8.1 三元表达式

js 复制代码
const text = isLogin ? '退出登录' : '立即登录'

适合简单的二选一,不要嵌套多层三元表达式。

8.2 &&||

js 复制代码
isLogin && loadUserInfo()
const displayName = nickname || username || '游客'

&& 左侧为真时才执行右侧;|| 返回第一个真值。业务代码中如果分支较复杂,普通 if 更容易阅读。

8.3 真值与假值

以下值在条件判断中为假:

js 复制代码
false
0
-0
0n
''
null
undefined
NaN

空数组 [] 和空对象 {} 都是真值:

js 复制代码
if ([]) {
  console.log('会执行')
}

9. 模块化:importexport

命名导出:

ts 复制代码
// userUtil.ts
export function formatUserName(name: string) {
  return name.trim()
}

export const DEFAULT_NAME = '游客'
ts 复制代码
// 使用方
import { DEFAULT_NAME, formatUserName } from './userUtil'

默认导出:

ts 复制代码
// appConfig.ts
export default {
  pageSize: 20,
}
ts 复制代码
import appConfig from './appConfig'

一个模块只能有一个默认导出,可以有多个命名导出。命名导入的名称必须对应,默认导入的本地名称可以自行命名。

Nuxt 项目会自动导入部分 Vue API、组合式函数和组件,所以有些文件中看不到 import。遇到不认识的标识符,先搜索项目或查看自动导入类型声明,不要默认它是浏览器原生能力。

10. Promise 与 async/await

Promise 表示一个未来才会完成或失败的异步结果:

js 复制代码
fetch('/api/users')
  .then(response => response.json())
  .then(data => {
    console.log(data)
  })
  .catch(error => {
    console.error(error)
  })

async/await 是更容易阅读的异步写法:

ts 复制代码
async function loadUsers() {
  try {
    const response = await fetch('/api/users')

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }

    const data = await response.json()
    console.log(data)
  }
  catch (error) {
    console.error('加载用户失败', error)
  }
}

注意事项:

  • async 函数始终返回 Promise。
  • await 只能直接写在 async 函数或支持顶层 await 的模块中。
  • 独立请求可以用 Promise.all 并行执行。
  • 存在前后依赖的请求必须按顺序等待。
ts 复制代码
const [users, roles] = await Promise.all([
  loadUsers(),
  loadRoles(),
])

不要在 forEach 中期待 await 按顺序等待:

ts 复制代码
// 错误理解:外层不会等待所有回调完成
items.forEach(async (item) => {
  await saveItem(item)
})

// 串行执行
for (const item of items) {
  await saveItem(item)
}

// 并行执行
await Promise.all(items.map(item => saveItem(item)))

11. Vue 单文件组件的基本结构

vue 复制代码
<script setup lang="ts">
const title = ref('用户列表')
</script>

<template>
  <main>
    <h1>{{ title }}</h1>
  </main>
</template>

<style scoped lang="scss">
main {
  padding: 16px;
}
</style>
  • <script setup>:状态、方法、组件逻辑。
  • <template>:页面结构和 Vue 模板语法。
  • <style scoped>:当前组件的局部样式。
  • lang="ts":脚本使用 TypeScript。
  • lang="scss":样式使用 SCSS。
  • scoped:样式选择器只作用于当前组件。

<script setup> 是 Vue 3 编译期语法糖,其中声明的变量、函数和导入的组件都可以直接在模板中使用。

12. Vue 模板常用语法

12.1 文本插值 {``{ }}

vue 复制代码
<p>{{ user.name }}</p>
<p>{{ count + 1 }}</p>
<p>{{ isLogin ? '已登录' : '未登录' }}</p>

插值内适合简单表达式,不要放赋值语句、循环或复杂业务逻辑。复杂结果应放入 computed

12.2 属性绑定 v-bind:

vue 复制代码
<img v-bind:src="avatarUrl" v-bind:alt="userName">
<img :src="avatarUrl" :alt="userName">

布尔属性:

vue 复制代码
<button :disabled="loading">提交</button>

动态 class:

vue 复制代码
<div :class="{ active: isActive, disabled: !enabled }" />
<div :class="['base', statusClass]" />

动态 style:

vue 复制代码
<div :style="{ color: textColor, fontSize: `${fontSize}px` }" />

同名属性可以进一步简写。以下写法需要当前 Vue 编译器支持:

vue 复制代码
<!-- :id="id" 的简写 -->
<div :id />

维护已有项目时,优先跟随项目当前写法;不确定编译版本时使用 :id="id" 最稳妥。

12.3 事件绑定 v-on@

vue 复制代码
<button v-on:click="handleClick">点击</button>
<button @click="handleClick">点击</button>
<button @click="deleteUser(user.id)">删除</button>

接收原生事件对象:

vue 复制代码
<input @input="handleInput">
<button @click="handleClick($event, user.id)">点击</button>
ts 复制代码
function handleInput(event: Event) {
  const target = event.target as HTMLInputElement
  console.log(target.value)
}

12.4 事件修饰符

vue 复制代码
<form @submit.prevent="submitForm">
  <button type="submit">保存</button>
</form>

<button @click.stop="handleButtonClick">阻止冒泡</button>
<button @click.once="handleOnce">只触发一次</button>
<input @keyup.enter="search">

常用修饰符:

修饰符 作用
.prevent 调用 event.preventDefault()
.stop 调用 event.stopPropagation()
.once 监听器最多触发一次
.self 只有点击元素自身才触发
.enter 按下回车键时触发

12.5 条件渲染 v-ifv-show

vue 复制代码
<p v-if="loading">加载中...</p>
<p v-else-if="errorMessage">{{ errorMessage }}</p>
<UserList v-else :users="users" />

<aside v-show="visible">侧边栏</aside>
指令 实际行为 适用场景
v-if 条件为假时不创建或销毁 DOM 条件较少变化、组件较重
v-show DOM 始终存在,只切换 display 频繁显示和隐藏

12.6 列表渲染 v-for

vue 复制代码
<ul>
  <li v-for="(user, index) in users" :key="user.id">
    {{ index + 1 }}. {{ user.name }}
  </li>
</ul>

对象遍历:

vue 复制代码
<div v-for="(value, key) in user" :key="key">
  {{ key }}:{{ value }}
</div>

注意事项:

  • v-for 必须提供稳定且唯一的 key
  • 优先使用数据库 ID,不要随意使用数组下标作为 key
  • 不建议在同一元素上同时写 v-ifv-for,先用 computed 过滤列表。
ts 复制代码
const enabledUsers = computed(() => {
  return users.value.filter(user => user.enabled)
})

12.7 双向绑定 v-model

vue 复制代码
<input v-model="keyword">
<textarea v-model="description" />
<input v-model="agreed" type="checkbox">

v-model 是"传值 + 监听更新事件"的语法糖。原生输入框可以近似理解为:

vue 复制代码
<input
  :value="keyword"
  @input="keyword = ($event.target as HTMLInputElement).value"
>

常用修饰符:

vue 复制代码
<input v-model.trim="name">
<input v-model.number="age" type="number">
<input v-model.lazy="description">
修饰符 作用
.trim 去除输入值首尾空格
.number 尝试将输入值转换成数字
.lazy 使用 change 事件而不是每次 input 都更新

Naive UI 常用具名 v-model

vue 复制代码
<n-input v-model:value="keyword" />
<n-modal v-model:show="modalVisible" />
<n-checkbox v-model:checked="agreed" />

v-model:value="keyword" 展开后是:

vue 复制代码
<n-input
  :value="keyword"
  @update:value="keyword = $event"
/>

12.8 v-html

vue 复制代码
<div v-html="htmlContent" />

v-html 会把字符串当作 HTML 插入。不要直接渲染用户输入或未经清洗的后端内容,否则可能产生 XSS 安全问题。普通文本一律使用 {``{ }}

13. Vue 响应式:refreactive

13.1 ref

ref 可以保存基本类型,也可以保存对象和数组:

ts 复制代码
const count = ref(0)
const keyword = ref('')
const users = ref<User[]>([])

count.value++
users.value.push({ id: 1, name: '张三' })

<script> 中需要 .value,在模板中会自动解包:

vue 复制代码
<p>{{ count }}</p>
<button @click="count++">加一</button>

13.2 reactive

reactive 只适合对象、数组等引用类型:

ts 复制代码
const form = reactive({
  name: '',
  age: 18,
})

form.name = '张三'

不要直接解构 reactive 对象的基本类型属性,否则会失去响应式联系:

ts 复制代码
const form = reactive({ name: '张三', age: 18 })

// name 和 age 只是普通变量,后续不会跟随 form 更新
const { name, age } = form

// 需要解构时使用 toRefs
const { name: reactiveName, age: reactiveAge } = toRefs(form)

13.3 怎么选择

场景 推荐
字符串、数字、布尔值 ref
可能整体替换的数组或对象 ref
属性较多、按字段修改的表单对象 reactive
团队希望统一心智模型 全部使用 ref 也可以

不要用普通变量保存需要驱动界面的状态:

ts 复制代码
let nonReactiveCount = 0 // 修改后不会触发模板更新
const reactiveCount = ref(0) // 正确

14. 派生状态:computed

当一个值可以根据其他响应式状态计算得出时,使用 computed

ts 复制代码
const firstName = ref('三')
const lastName = ref('张')

const fullName = computed(() => {
  return `${lastName.value}${firstName.value}`
})
vue 复制代码
<p>{{ fullName }}</p>

列表搜索示例:

ts 复制代码
const keyword = ref('')
const users = ref<User[]>([])

const filteredUsers = computed(() => {
  const normalizedKeyword = keyword.value.trim().toLowerCase()

  if (!normalizedKeyword) {
    return users.value
  }

  return users.value.filter(user =>
    user.name.toLowerCase().includes(normalizedKeyword),
  )
})

computed 有缓存,依赖不变时不会重复计算。它应该负责"计算结果",不要在其中发请求、改其他状态或操作 DOM。

15. 监听变化:watchwatchEffect

15.1 watch

ts 复制代码
watch(keyword, (newValue, oldValue) => {
  console.log('关键词变化', oldValue, '->', newValue)
})

监听对象的某个属性:

ts 复制代码
watch(
  () => form.userId,
  (userId) => {
    loadUserDetail(userId)
  },
)

监听多个来源:

ts 复制代码
watch([keyword, page], ([newKeyword, newPage]) => {
  console.log(newKeyword, newPage)
})

常用配置:

ts 复制代码
watch(
  () => form.filters,
  () => {
    search()
  },
  {
    deep: true,
    immediate: true,
  },
)
  • immediate: true:创建监听后立即执行一次。
  • deep: true:深度监听嵌套属性;大型对象使用时要注意性能。

15.2 watchEffect

ts 复制代码
watchEffect(() => {
  document.title = `${title.value}(${count.value})`
})

watchEffect 会自动收集执行期间读取的响应式依赖,并立即运行一次。需要明确新旧值、精确控制依赖时使用 watch

15.3 与 computed 的区别

API 用途
computed 根据状态计算另一个值
watch 状态变化后执行请求、日志、存储等副作用
watchEffect 自动收集依赖并执行副作用

16. 生命周期

ts 复制代码
onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
生命周期 常见用途
onBeforeMount 组件挂载到页面前的准备
onMounted 访问 DOM、绑定浏览器事件、初始化第三方库
onBeforeUpdate Vue 更新 DOM 前读取旧状态,较少使用
onUpdated DOM 更新完成后的处理,避免在这里持续修改状态
onBeforeUnmount 组件销毁前处理收尾工作
onUnmounted 清除事件、定时器、订阅和第三方实例

等待本轮状态变化反映到 DOM:

ts 复制代码
async function openEditor() {
  visible.value = true
  await nextTick()
  editorRef.value?.focus()
}

请求不依赖 DOM 时不必机械地全部放进 onMounted。在 Nuxt 中访问 windowdocument 等浏览器对象时,要确认代码只在客户端执行。

17. 父子组件通信:Props 与 Emit

17.1 父组件向子组件传值

子组件:

vue 复制代码
<script setup lang="ts">
interface Props {
  userId: number
  title?: string
}

const props = withDefaults(defineProps<Props>(), {
  title: '用户详情',
})
</script>

<template>
  <h2>{{ props.title }}</h2>
  <p>用户 ID:{{ props.userId }}</p>
</template>

父组件:

vue 复制代码
<UserDetail :user-id="currentUserId" title="个人资料" />

要点:

  • JavaScript 中的 userId 在模板属性中通常写成 user-id
  • Props 是只读输入,子组件不要直接修改。
  • definePropswithDefaults 是编译期宏,不需要手动导入。

17.2 子组件通知父组件

子组件:

vue 复制代码
<script setup lang="ts">
const emit = defineEmits<{
  saved: [id: number]
  cancelled: []
}>()

/**
 * 通知父组件当前用户已经保存成功。
 *
 * @param id 已保存的用户 ID
 * @returns 无返回值
 */
function notifySaved(id: number) {
  emit('saved', id)
}
</script>

<template>
  <button @click="notifySaved(1)">保存</button>
  <button @click="emit('cancelled')">取消</button>
</template>

父组件:

vue 复制代码
<UserEditor
  @saved="handleSaved"
  @cancelled="editorVisible = false"
/>

数据通常向下通过 Props 传递,事件向上通过 Emit 通知,这叫"单向数据流"。

18. 组件上的 v-model

父组件:

vue 复制代码
<UserNameInput v-model="userName" />

它展开后是:

vue 复制代码
<UserNameInput
  :model-value="userName"
  @update:model-value="userName = $event"
/>

子组件的兼容写法:

vue 复制代码
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

/**
 * 将输入框的新值通知给父组件,完成组件双向绑定。
 *
 * @param event 原生输入事件
 * @returns 无返回值
 */
function handleInput(event: Event) {
  const target = event.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>

<template>
  <input :value="props.modelValue" @input="handleInput">
</template>

较新的 Vue 版本还支持 defineModel 语法糖:

vue 复制代码
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>

<template>
  <input v-model="model">
</template>

使用 defineModel 前要确认项目的 Vue 和编译器版本支持。维护老项目时,Props + update:modelValue 的写法兼容性更明确。

具名 v-model

vue 复制代码
<!-- 父组件 -->
<UserDialog v-model:show="dialogVisible" />
ts 复制代码
// 子组件的事件约定
const props = defineProps<{ show: boolean }>()
const emit = defineEmits<{
  'update:show': [value: boolean]
}>()

19. 插槽 Slot

插槽让父组件把一段模板传给子组件。

子组件:

vue 复制代码
<template>
  <section class="panel">
    <header>
      <slot name="header" />
    </header>

    <slot />
  </section>
</template>

父组件:

vue 复制代码
<BasePanel>
  <template #header>
    <h2>用户信息</h2>
  </template>

  <p>这里是默认插槽内容</p>
</BasePanel>

#headerv-slot:header 的简写。

作用域插槽允许子组件把数据交给父组件模板:

vue 复制代码
<!-- 子组件 -->
<slot name="item" :user="user" :index="index" />
vue 复制代码
<!-- 父组件 -->
<template #item="{ user, index }">
  {{ index + 1 }}. {{ user.name }}
</template>

20. 模板引用 ref

访问 DOM:

vue 复制代码
<script setup lang="ts">
const inputRef = ref<HTMLInputElement | null>(null)

/**
 * 让搜索输入框获得焦点。
 *
 * @returns 无返回值
 */
function focusInput() {
  inputRef.value?.focus()
}
</script>

<template>
  <input ref="inputRef">
  <button @click="focusInput">聚焦</button>
</template>

访问子组件公开的方法时,子组件需要显式暴露:

ts 复制代码
// 子组件
function reset() {
  // 重置组件内部状态
}

defineExpose({ reset })

defineExpose 也是 <script setup> 编译期宏。

21. 跨层组件通信:provideinject

祖先组件:

ts 复制代码
const theme = ref<'light' | 'dark'>('light')

provide('theme', theme)

后代组件:

ts 复制代码
const theme = inject<Ref<'light' | 'dark'>>('theme')

适用于主题、表单上下文、组件库上下文等"同一组件树内跨多层共享"的数据。跨页面、跨布局的全局业务状态通常更适合 Pinia。

大型项目建议使用 InjectionKey,避免字符串 key 冲突并获得完整类型提示。

22. TypeScript 在 Vue 中的常用写法

22.1 基础类型

ts 复制代码
const name = ref<string>('')
const age = ref<number>(0)
const loading = ref<boolean>(false)
const selectedId = ref<number | null>(null)

能够自动推断时不必重复标注:

ts 复制代码
const name = ref('')
const loading = ref(false)

空数组通常需要明确元素类型:

ts 复制代码
interface User {
  id: number
  name: string
  enabled: boolean
}

const users = ref<User[]>([])

22.2 interfacetype

ts 复制代码
interface User {
  id: number
  name: string
  avatar?: string
}

type Status = 'idle' | 'loading' | 'success' | 'error'
type UserId = number | string
  • 对象结构常用 interface
  • 联合类型、工具类型、别名常用 type
  • ? 表示属性可选。

22.3 避免滥用 any

ts 复制代码
const data = ref<any>(null) // 类型检查基本失效

未知外部数据优先使用 unknown,验证后再使用:

ts 复制代码
function printMessage(value: unknown) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase())
  }
}

类型断言不是数据转换:

ts 复制代码
const input = event.target as HTMLInputElement

as 只是告诉 TypeScript"按这个类型检查",不会在运行时把错误数据变正确。

23. 一个完整的 Vue 列表示例

vue 复制代码
<script setup lang="ts">
interface User {
  id: number
  name: string
  enabled: boolean
}

const keyword = ref('')
const loading = ref(false)
const users = ref<User[]>([
  { id: 1, name: '张三', enabled: true },
  { id: 2, name: '李四', enabled: false },
])

const filteredUsers = computed(() => {
  const normalizedKeyword = keyword.value.trim().toLowerCase()

  if (!normalizedKeyword) {
    return users.value
  }

  return users.value.filter(user =>
    user.name.toLowerCase().includes(normalizedKeyword),
  )
})

/**
 * 切换指定用户的启用状态。
 *
 * @param userId 需要切换状态的用户 ID
 * @returns 无返回值
 */
function toggleUser(userId: number) {
  const user = users.value.find(item => item.id === userId)

  if (user) {
    user.enabled = !user.enabled
  }
}

/**
 * 删除指定用户。
 *
 * @param userId 需要删除的用户 ID
 * @returns 无返回值
 */
function deleteUser(userId: number) {
  users.value = users.value.filter(user => user.id !== userId)
}
</script>

<template>
  <main class="user_page">
    <input v-model.trim="keyword" placeholder="请输入用户名">

    <p v-if="loading">
      加载中...
    </p>

    <p v-else-if="filteredUsers.length === 0">
      暂无数据
    </p>

    <ul v-else>
      <li v-for="user in filteredUsers" :key="user.id">
        <span :class="{ disabled: !user.enabled }">
          {{ user.name }}
        </span>

        <button @click="toggleUser(user.id)">
          {{ user.enabled ? '禁用' : '启用' }}
        </button>

        <button @click="deleteUser(user.id)">
          删除
        </button>
      </li>
    </ul>
  </main>
</template>

<style scoped>
.user_page {
  padding: 16px;
}

.disabled {
  color: #999;
  text-decoration: line-through;
}
</style>

这个例子串联了:

  • interface 数据类型。
  • ref 响应式状态。
  • computed 派生列表。
  • v-model.trim 输入绑定。
  • v-if / v-else-if / v-else 状态展示。
  • v-for 列表渲染。
  • :class 动态样式。
  • @click 事件绑定。
  • findfilter 和箭头函数。

24. 本项目中最常见的写法

24.1 Vue 与 Nuxt API 自动导入

本项目配置了自动导入,因此通常可以直接使用:

ts 复制代码
const route = useRoute()
const router = useRouter()
const keyword = ref('')
const count = computed(() => keyword.value.length)

不需要在每个文件中重复编写:

ts 复制代码
import { computed, ref } from 'vue'

但自动导入是 Nuxt 的构建能力,不是 JavaScript 本身的语法。

24.2 页面元信息

ts 复制代码
definePageMeta({
  title: '用户管理',
  layout: 'backstage',
  name: 'userManage',
  authKey: 'UserManage',
})

definePageMeta 是 Nuxt 编译宏,用于配置页面标题、布局、路由名和项目权限信息。

24.3 Naive UI 的具名双向绑定

vue 复制代码
<n-input v-model:value="form.name" />
<n-select v-model:value="form.roleId" :options="roleOptions" />
<n-switch v-model:value="form.enabled" />
<n-modal v-model:show="modalVisible" />

不要把 Naive UI 的 v-model:value 机械地改成原生输入框常见的 v-model,组件的属性和更新事件名称由组件库约定。

24.4 项目 API 请求

本项目业务请求优先使用 useAppFetch,不要在页面中随意重复封装 fetch 或 Axios:

ts 复制代码
const loading = ref(false)
const rows = ref<User[]>([])
const { useLoadingRequest } = useAppFetch()

/**
 * 加载用户列表,并同步页面加载状态和列表数据。
 *
 * @returns 请求完成后的 Promise
 */
async function loadUsers() {
  await useLoadingRequest({
    k: 'backstage/userList',
    method: 'get',
    params: {
      keyword: keyword.value,
    },
    loadings: [loading],
    state: rows,
  })
}

接口 key 和准确参数以实际模块配置与后端接口为准。

25. 新手高频错误

25.1 忘记 .value

ts 复制代码
const count = ref(0)

count++ // 错误
count.value++ // 正确

模板中不需要 .value

vue 复制代码
<p>{{ count }}</p>

25.2 直接修改 Props

ts 复制代码
const props = defineProps<{ count: number }>()

props.count++ // 错误:Props 是只读的

应通过 emit 通知父组件,或复制到本地状态后明确管理同步关系。

25.3 把 computed 当普通变量修改

ts 复制代码
const fullName = computed(() => `${lastName.value}${firstName.value}`)

// fullName.value = '李四' // 只读 computed 不能直接修改

25.4 v-for 使用不稳定的 key

vue 复制代码
<!-- 列表会增删或排序时不推荐 -->
<li v-for="(user, index) in users" :key="index" />

<!-- 推荐 -->
<li v-for="user in users" :key="user.id" />

25.5 用 map 只做副作用

js 复制代码
// 不合适:创建了一个没有使用的新数组
users.map(user => console.log(user.name))

// 合适
users.forEach(user => console.log(user.name))

25.6 异步请求缺少错误和加载状态

页面请求至少要考虑:

  • 加载中。
  • 请求失败。
  • 空数据。
  • 重复提交。
  • 组件卸载后的请求影响。

在本项目中优先通过 useAppFetch 的请求方法统一处理这些状态。

25.7 误解对象和数组比较

js 复制代码
{} === {} // false
[] === [] // false

对象和数组按引用比较,不是按内容比较。判断空数组使用 list.length === 0,判断对象字段应比较具体字段或使用可靠的深比较工具。

25.8 混淆 nullundefined

  • undefined:通常表示尚未赋值或字段不存在。
  • null:通常表示业务上明确"当前没有值"。

团队接口中应保持统一口径,避免同一字段有时缺失、有时为空字符串、有时为 null

26. 常用语法糖速查表

简写 完整写法或含义
:title="title" v-bind:title="title"
@click="submit" v-on:click="submit"
#header v-slot:header
v-model="name" :model-value + @update:model-value
v-model:value="name" :value + @update:value
{ name } { name: name }
method() {} method: function () {}
你好,${name} 字符串拼接
const { id } = user 读取 user.id 到变量 id
const [first] = list 读取 list[0] 到变量 first
{ ...source } 浅拷贝对象属性
[...source] 浅拷贝数组元素
(...args) 收集剩余参数为数组
value => value.id 单参数箭头函数
user?.profile?.name 遇到 null/undefined 时停止访问
value ?? defaultValue 仅在空值时使用默认值
<script setup> Vue 组合式 API 的编译期简化写法
defineProps <script setup> 的 Props 编译宏
defineEmits <script setup> 的 Emit 编译宏
defineExpose 暴露内容给父组件模板引用
defineModel 较新 Vue 中声明组件 v-model 的编译宏

27. 推荐学习顺序与练习

第一阶段:能看懂页面

学习内容:

  1. constlet、函数、对象和数组。
  2. 模板字符串、解构、展开、箭头函数。
  3. mapfilterfindsome
  4. async/awaittry/catch
  5. Vue 的 {``{ }}:@v-ifv-forv-model

练习:用静态数组渲染用户列表,增加搜索和启用状态筛选。

第二阶段:能修改组件

学习内容:

  1. refreactivecomputedwatch
  2. Props、Emit、组件 v-model
  3. 生命周期和模板引用。
  4. TypeScript 的 interface、联合类型和空值类型。

练习:把用户列表拆成父页面、列表组件和编辑弹窗三个组件。

第三阶段:能开发业务页面

学习内容:

  1. Nuxt 页面、布局、路由和自动导入。
  2. 本项目 useAppFetch 请求封装。
  3. Naive UI 与项目公共组件。
  4. Pinia 状态管理。
  5. loading、空状态、错误状态、权限和表单校验。

练习:参考项目现有页面,实现一个包含查询、新增、编辑、删除的完整管理功能。

28. 阅读代码时的快速判断法

看到一段代码时,按以下问题依次判断:

  1. 数据从哪里来:本地变量、Props、Pinia 还是接口?
  2. 它是否响应式:refreactivecomputed 还是普通变量?
  3. 谁会修改它:当前组件、父组件还是请求回调?
  4. 模板如何使用它:插值、属性、条件、循环还是事件?
  5. 修改后影响哪里:当前元素、整个列表、子组件还是全局状态?
  6. 异步过程是否覆盖加载、失败、空数据和重复操作?

把这六个问题回答清楚,大多数 Vue 页面就不再只是"看起来会动的 HTML",而是一条可以追踪的数据流。

相关推荐
dsyyyyy11011 小时前
用JavaScript实现排序算法
开发语言·javascript·排序算法
槑有老呆1 小时前
从零搭建 LLM 流式对话前端:一文读懂 SSE、二进制流与 Vue 3 响应式
前端·javascript
没落英雄1 小时前
7. 从零开始搭建一个 AI Agent —— UI 卡片渲染系统
前端·人工智能·架构
敖行客Allthinker1 小时前
Vuex 状态响应机制:watch 与 subscribe 的对比分析
vue.js
前端缘梦1 小时前
LangGraph 核心特性技术详解:流式输出、持久化、记忆体系与中断实战。
前端·人工智能·程序员
张龙6871 小时前
前端错误监控生产级方案:从 onerror 捕获到 Source Map 精准还原
前端·javascript
渣波1 小时前
从零手写 Vite Mock 流式 AI 对话,搞懂 LLM 的“打字机”特效
前端·javascript
温柔过期啦1 小时前
网站改了标题但是搜索引擎却没改?怎么样才能快速更新?
前端·搜索引擎·百度
用户2930750976691 小时前
Vue3 流式 AI 对话:深入理解两个 done 与 buffer 缓存机制
vue.js