1. 插槽语法统一(v-slot)

Vue3 中统一使用v-slot指令,替代了之前的slot属性写法:

vue

<!-- 父组件 -->
<template>
  <ChildComponent>
    <!-- 具名插槽 -->
    <v-slot:default>默认插槽内容</v-slot:default>
    
    <!-- 具名插槽 -->
    <v-slot:header>头部内容</v-slot:header>
    
    <!-- 作用域插槽 -->
    <v-slot:item="slotProps">
      {{ slotProps.data }}
    </v-slot:item>
  </ChildComponent>
</template>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="item" :data="message"></slot>
  </div>
</template>

<script setup>
const message = "来自子组件的数据"
</script>

2. Render 函数变化

Vue3 的 render 函数更贴合组合式 API 风格:

vue

<template>
  <div>
    <render-component />
  </div>
</template>

<script setup>
import { h } from 'vue'

// 定义一个使用render函数的组件
const RenderComponent = {
  render() {
    // 返回虚拟DOM节点
    return h('div', { 
      class: 'render-example',
      onClick: () => console.log('点击了')
    }, [
      h('h3', '这是render函数创建的元素'),
      h('p', 'Vue3的render函数更简洁')
    ])
  }
}
</script>

3. 全局 API 使用方式

Vue3 通过createApp创建应用实例,避免全局污染:

javascript

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 创建应用实例
const app = createApp(App)

// 使用插件
app.use(router)
app.use(store)

// 全局注册组件
import GlobalComponent from './components/GlobalComponent.vue'
app.component('GlobalComponent', GlobalComponent)

// 全局注册指令
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// 挂载应用
app.mount('#app')

4. Teleport 组件使用

teleport可以将组件内容渲染到指定 DOM 节点:

vue

<template>
  <div>
    <button @click="showModal = true">打开模态框</button>
    
    <teleport to="body">
      <div v-if="showModal" class="modal-backdrop">
        <div class="modal">
          <h3>这是一个模态框</h3>
          <p>内容会被传送到body节点下</p>
          <button @click="showModal = false">关闭</button>
        </div>
      </div>
    </teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

<style>
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal {
  background: white;
  padding: 20px;
  border-radius: 5px;
}
</style>

5. Composition API 示例

Composition API 提供了更灵活的逻辑组织方式:

vue

<template>
  <div>
    <h3>用户列表</h3>
    <div v-if="loading">加载中...</div>
    <div v-if="error">{{ error }}</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
    <button @click="fetchUsers">刷新用户</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 定义响应式数据
const users = ref([])
const loading = ref(false)
const error = ref(null)

// 定义逻辑函数
const fetchUsers = async () => {
  try {
    loading.value = true
    error.value = null
    const response = await fetch('https://api.example.com/users')
    const data = await response.json()
    users.value = data
  } catch (err) {
    error.value = '获取用户失败,请重试'
    console.error(err)
  } finally {
    loading.value = false
  }
}

// 生命周期钩子
onMounted(() => {
  fetchUsers()
})
</script>

********************关于TS***************************

如何用ts在vue3中自定义组件

1. 基础组件定义(使用 <script setup lang="ts">)

这是 Vue 3 推荐的方式,结合了 <script setup> 的简洁性和 TypeScript 的类型安全。

<template>
  <button 
    class="custom-button"
    :class="{ 'is-disabled': disabled, 'is-primary': type === 'primary' }"
    @click="$emit('click')"
    :disabled="disabled"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
// 定义组件 props 类型
const props = defineProps<{
  // 按钮类型,可选值:default/primary
  type?: 'default' | 'primary'
  // 是否禁用
  disabled?: boolean
  // 按钮尺寸
  size?: 'small' | 'medium' | 'large'
}>()

// 定义组件 emits 类型
const emit = defineEmits<{
  // 点击事件
  (e: 'click'): void
  // 自定义事件,携带数字参数
  (e: 'custom-event', value: number): void
}>()

// 示例方法:触发自定义事件
const handleCustomEvent = () => {
  if (!props.disabled) {
    emit('custom-event', Math.random())
  }
}
</script>

<style scoped>
.custom-button {
  padding: 8px 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}

.custom-button.is-primary {
  background-color: #42b983;
  color: white;
  border-color: #42b983;
}

.custom-button.is-disabled {
  cursor: not-allowed;
  opacity: 0.7;
}
</style>
    

2. 使用 defineComponent 定义组件(适用于更复杂的场景)

对于需要更复杂逻辑的组件,可以使用 defineComponent 显式定义组件选项。

<template>
  <div class="card" :style="cardStyle">
    <div class="card-header" v-if="title">
      <h3>{{ title }}</h3>
    </div>
    <div class="card-body">
      <slot />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'

// 定义 props 接口
interface CardProps {
  // 卡片标题
  title?: string
  // 是否有阴影
  shadow?: boolean
  // 自定义样式
  customStyle?: Record<string, string>
}

// 定义 emits 接口
interface CardEmits {
  (e: 'header-click'): void
}

export default defineComponent<CardProps, CardEmits>({
  name: 'Card', // 显式指定组件名(利于调试和 IDE 提示)
  
  props: {
    title: {
      type: String,
      default: ''
    },
    shadow: {
      type: Boolean,
      default: false
    },
    customStyle: {
      type: Object as () => Record<string, string>,
      default: () => ({})
    }
  },
  
  emits: ['header-click'],
  
  setup(props, { emit }) {
    // 计算属性(带类型推断)
    const cardStyle = computed(() => ({
      boxShadow: props.shadow ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
      ...props.customStyle
    }))
    
    // 处理标题点击
    const handleHeaderClick = () => {
      emit('header-click')
    }
    
    return {
      cardStyle,
      handleHeaderClick
    }
  }
})
</script>

<style scoped>
.card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
  margin: 8px 0;
}

.card-header {
  padding-bottom: 8px;
  border-bottom: 1px solid #eee;
  cursor: pointer;
}
</style>
    

3. 组件的使用示例

在父组件中使用上述自定义组件时,TypeScript 会提供完整的类型提示:

<template>
  <div class="demo-page">
    <!-- 使用 Button 组件 -->
    <Button 
      type="primary" 
      size="medium"
      @click="handleButtonClick"
      @custom-event="handleCustomEvent"
    >
      点击我
    </Button>
    
    <!-- 使用 Card 组件 -->
    <Card 
      title="我的卡片" 
      :shadow="true"
      :custom-style="{ marginTop: '20px' }"
      @header-click="handleCardHeaderClick"
    >
      <p>这是卡片内容</p>
    </Card>
  </div>
</template>

<script setup lang="ts">
import Button from '../components/Button.vue'
import Card from '../components/Card.vue'

const handleButtonClick = () => {
  console.log('按钮被点击')
}

const handleCustomEvent = (value: number) => {
  console.log('收到自定义事件值:', value)
}

const handleCardHeaderClick = () => {
  console.log('卡片标题被点击')
}
</script>
    

关键知识点总结

  1. Props 类型定义

    • 使用 defineProps<{...}> 进行类型约束(推荐 <script setup> 方式)
    • 支持联合类型(如 type?: 'default' | 'primary')
    • 支持可选属性(添加 ? 后缀)
  2. Emits 类型定义

    • 使用 defineEmits<{...}> 定义事件类型和参数
    • 格式:(e: '事件名', 参数1: 类型1, 参数2: 类型2) => void
  3. 组件通信

    • Props 用于父传子数据
    • Emits 用于子传父事件
    • 类型系统会自动检查传递的 props 和事件是否符合定义
  4. 高级类型技巧

    • 可以提取接口(Interface)复用类型定义
    • 使用 Record<string, any> 定义对象类型
    • 对复杂类型使用 as 进行类型断言

一、高泛用性的API

(一)、ref 和 reactive 的区别

ref 和 reactive 是 Vue3 中用于创建响应式数据的核心 API,核心区别在于处理的数据类型和访问方式,实际使用中需根据数据类型选择:

1. 数据类型差异
  • ref:用于处理基本类型数据(String/Number/Boolean/Null/Undefined),也可处理对象 / 数组(但内部会自动转换为 reactive 对象)。
  • reactive:仅用于处理对象或数组(引用类型),无法直接处理基本类型(会报警告且不响应)。
2. 访问与修改方式
  • ref:通过 .value 访问 / 修改原始值(模板中自动解包,无需 .value)。
  • reactive:直接通过属性访问 / 修改(类似普通对象),但不能直接赋值为新对象(会丢失响应式)。
3. 实际例子
<template>
  <div>
    <!-- ref 在模板中自动解包,无需 .value -->
    <p>ref数字: {{ count }}</p>
    <p>reactive对象: {{ user.name }}</p>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

// 基本类型用 ref
const count = ref(0)
// 修改时需 .value
count.value += 1 // 正确:响应式生效

// 对象类型用 reactive
const user = reactive({ name: '张三', age: 20 })
// 直接修改属性
user.age = 21 // 正确:响应式生效

// ❌ 错误:reactive 不能直接赋值新对象(会丢失响应式)
user = { name: '李四' } // 警告 + 非响应式

// ref 处理对象(内部转为 reactive)
const info = ref({ address: '北京' })
info.value.address = '上海' // 正确:响应式生效(需 .value 访问对象)
</script>
4. 最佳实践
  • 基本类型 / 单值数据(如数字、字符串)→ 用 ref。
  • 复杂对象 / 数组 → 用 reactive(更符合对象的自然操作方式)。
  • 注意:reactive 对象解构后会丢失响应式,需用 toRefs 转换(const { name } = toRefs(user))。

(二)、watch 和 watchEffect 的区别

watch 和 watchEffect 都是 Vue3 中用于监听数据变化的 API,核心区别在于监听方式和触发时机,实际使用中需根据场景选择:

1. 监听方式
  • watch:显式指定监听源(需明确告诉 Vue 要监听哪个数据),且能获取新旧值。
  • watchEffect:自动收集依赖(无需指定监听源,Vue 会自动追踪回调中使用的响应式数据),但无法直接获取新旧值。
2. 触发时机
  • watch:默认惰性执行(初始渲染时不执行,仅在监听源变化时执行)。
  • watchEffect:立即执行(初始渲染时会先执行一次,然后追踪依赖变化)。

3. 实际例子

<script setup>
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const user = ref({ name: '张三', age: 20 })

// 1. watch:显式监听,获取新旧值,惰性执行
watch(
  count, // 显式指定监听源
  (newVal, oldVal) => {
    console.log('count变化:', newVal, oldVal) // 初始不执行,count变了才执行
  }
)

// 监听对象的某个属性(需用函数返回)
watch(
  () => user.value.age, // 显式指定监听 user.age
  (newAge, oldAge) => {
    console.log('年龄变化:', newAge, oldAge)
  }
)

// 2. watchEffect:自动收集依赖,无新旧值,立即执行
watchEffect(() => {
  // 自动监听 count 和 user.age(因为回调中用到了它们)
  console.log('依赖变化:', count.value, user.value.age)
  // 初始会执行一次(打印初始值),之后 count 或 age 变了就执行
})
</script>
4. 适用场景
  • watch:

    • 需要明确知道监听的数据源(如表单提交前验证某个字段)。
    • 需要获取新旧值对比(如监控数值变化幅度)。
    • 需要惰性执行(如初始化时不需要执行,仅变化时执行)。
  • watchEffect:

    • 依赖关系复杂,难以明确指定监听源(如同时依赖多个响应式数据)。
    • 需要初始化时立即执行(如页面加载时请求数据,后续依赖变化重新请求)。

总结

  • ref vs reactive:看数据类型,基本类型用 ref(带 .value),对象数组用 reactive(直接操作属性)。
  • watch vs watchEffect:看是否需要显式指定源和新旧值(选 watch),还是自动收集依赖并立即执行(选 watchEffect)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐