一文吃透 Vue3 + TS 核心语法:ref、reactive、props、emit、watch、Pinia 全解析

Vue 3 + TypeScript常用核心概念的系统性总结与最佳实践,适合放在手边的实用工具文档。


一、响应式数据:refreactive

1. ref

用于定义基本类型需要响应式解包的对象引用

import { ref } from 'vue'

const count = ref<number>(0)
const name = ref<string>('Vue')

// 修改
count.value++

// 在模板中自动解包:{{ count }}

⚠️ 注意:在 JS/TS 中必须用 .value,模板中自动解包。


2. reactive

用于定义响应式对象(不推荐用于基本类型)。

import { reactive } from 'vue'

interface User {
  name: string
  age: number
}

const user = reactive<User>({
  name: 'Alice',
  age: 20
})

user.age++ // 直接操作,无需 .value

❗ 注意:reactive 不会为基本类型创建响应式,且解构会失去响应性。


3. ref vs reactive 使用建议

场景推荐
基本类型(number, string, boolean)ref
对象或复杂结构reactiveref<object>
需要解构赋值ref + .value 更安全
想在 setup 返回多个变量ref 更方便

二、props:父子组件传值(TS 类型安全)

<!-- Child.vue -->
<script setup lang="ts">
interface Props {
  title: string
  disabled?: boolean
  count?: number
}

// 定义默认值 + 类型
const props = withDefaults(defineProps<Props>(), {
  disabled: false,
  count: 0
})
</script>

<template>
  <div :class="{ disabled }">{{ title }} ({{ count }})</div>
</template>
<!-- Parent.vue -->
<template>
  <Child title="Hello" :count="5" />
</template>

要点:

  • 使用 defineProps + withDefaults 实现类型安全和默认值
  • 避免使用 any,始终定义接口

三、emit:子组件向父组件触发事件

<!-- Child.vue -->
<script setup lang="ts">
// 定义触发的事件
const emit = defineEmits<{
  (e: 'update', id: number): void
  (e: 'close'): void
}>()

// 使用
function handleClick() {
  emit('update', 123)
}
</script>
<!-- Parent.vue -->
<Child @update="onUpdate" @close="onClose" />

类型安全:使用对象语法定义事件签名,避免字符串拼写错误。


四、computed:计算属性

import { computed, ref } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 可写 computed(少见)
const writableName = computed({
  get: () => fullName.value,
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})

特点:缓存、依赖自动追踪、性能优化。


五、watchwatchEffect:侦听变化

1. watch:侦听特定数据源

import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`count changed: ${oldVal} -> ${newVal}`)
})

// 侦听多个源
watch([count, name], ([c, n], [oldC, oldN]) => {
  // ...
})

// 深层侦听对象
watch(
  () => user.profile,
  (newVal) => {
    console.log('profile changed')
  },
  { deep: true }
)

2. watchEffect:自动追踪依赖,立即执行

watchEffect(() => {
  console.log('Current count:', count.value)
  // 自动监听 count.value 变化
})

适用:副作用逻辑,如日志、请求、DOM 操作。


六、组件通信:defineExpose + template ref

子组件暴露方法/属性

<!-- Child.vue -->
<script setup lang="ts">
const scrollIntoView = () => {
  // 滚动逻辑
}

defineExpose({
  scrollIntoView
})
</script>

父组件获取子组件实例

<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref<InstanceType<typeof Child> | null>(null)

const handleScroll = () => {
  childRef.value?.scrollIntoView()
}
</script>

<template>
  <Child ref="childRef" />
  <button @click="handleScroll">滚动到视图</button>
</template>

注意:

  • 必须用 defineExpose 才能暴露
  • TS 中建议用 InstanceType<typeof Component> 类型推断

七、插槽 slot:内容分发

1. 默认插槽

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>Header</header>
    <main>
      <slot></slot>
    </main>
  </div>
</template>
<!-- 使用 -->
<Layout>
  <p>这是主体内容</p>
</Layout>

2. 具名插槽

<!-- Layout.vue -->
<slot name="header"></slot>
<slot name="sidebar"></slot>
<slot></slot>
<Layout>
  <template #header>
    <h1>自定义标题</h1>
  </template>
  <template #sidebar>
    <nav>菜单</nav>
  </template>
  <p>主内容</p>
</template>

3. 作用域插槽(传数据给父组件)

<!-- List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index"></slot>
    </li>
  </ul>
</template>
<List>
  <template #default="{ item, index }">
    <span>{{ index }}: {{ item.name }}</span>
  </template>
</template>

适用于可定制组件(如表格、列表)


八、Pinia 状态管理(替代 Vuex)

1. 安装与配置

npm install pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

2. 创建 Store

// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: 'Alice',
    age: 20,
    hobbies: ['reading'] as string[]
  }),

  getters: {
    doubleAge: (state) => state.age * 2,
    info: (state) => `${state.name}, ${state.age}`
  },

  actions: {
    updateName(name: string) {
      this.name = name
    },
    async fetchUserData() {
      const res = await api.getUser()
      this.$patch(res) // 批量更新
    }
  }
})

3. 在组件中使用

<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 读取
console.log(userStore.name)

// 修改
userStore.updateName('Bob')

// 解构(保持响应式)
import { storeToRefs } from 'pinia'
const { name, age } = storeToRefs(userStore)
const { updateName } = userStore
</script>

storeToRefs:解构时保持响应式,不会丢失追踪。


九、其他常用技巧

1. onMounted, onUnmounted 等生命周期

import { onMounted, onUnmounted, onBeforeUnmount } from 'vue'

onMounted(() => {
  console.log('组件挂载')
})

onBeforeUnmount(() => {
  // 清理定时器、事件监听
})

2. nextTick:等待 DOM 更新后执行

import { nextTick } from 'vue'

await nextTick()
// DOM 已更新

3. defineOptions(可选,用于 defineSetup 辅助)

<script setup lang="ts">
defineOptions({
  name: 'MyComponent',
  inheritAttrs: false
})
</script>

十、项目结构建议(推荐)

src/
├── components/       # 通用组件
├── views/            # 页面组件
├── composables/      # 自定义 Hook(useXXX)
├── stores/           # Pinia stores
├── utils/            # 工具函数
├── api/              # 接口请求
├── types/            # TS 类型定义
├── assets/           # 静态资源
└── App.vue / main.ts

总结:Vue 3 + TS 核心要点速查表

功能推荐方式说明
响应式数据ref / reactive基本类型用 ref,对象用 reactive
父子传值defineProps + defineEmitsTS 类型安全
计算属性computed缓存、性能好
侦听变化watch / watchEffect精准或自动依赖
获取子组件ref + defineExpose需暴露才能访问
内容分发slot支持默认、具名、作用域插槽
状态管理Pinia简洁、类型友好、模块化
生命周期onMountedComposition API 风格
最佳实践script setup + TS减少模板代码,类型安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sherry Tian

打赏1元鼓励作者

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值