普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月2日首页

Vue 组件通信的 8 种最佳实践,你知道几种?

作者 刘大华
2026年1月2日 17:11

经常写 Vue 的朋友应该很熟悉,在 Vue 的应用中,组件化开发可以让我们的代码更容易维护,而组件之间的数据传递事件通信也是我们必须要解决的问题。

经过多个项目的实践,我逐渐摸清了Vue3中8种组件通信方式和适用场景。

下面来给大家分享一下。

1. Props / Emits:最基础的父子传值

这是 Vue 的官方推荐通信方式,遵循单向数据流原则,数据只能从上往下流,事件从下往上传。

Props:父传子的单向数据流

适用场景:当你需要把配置、用户信息、状态等数据从父组件传递给子组件时。

<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <!-- 传递静态和动态数据 -->
    <ChildComponent 
      title="用户信息" 
      :user="userData"
      :count="clickCount"
    />
  </div>
</template>

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

const userData = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com'
})

const clickCount = ref(0)
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <h3>{{ title }}</h3>
    <div class="user-card">
      <p>姓名:{{ user.name }}</p>
      <p>年龄:{{ user.age }}</p>
      <p>邮箱:{{ user.email }}</p>
    </div>
    <p>点击次数:{{ count }}</p>
  </div>
</template>

<script setup>
// 方式1:简单定义
// defineProps(['title', 'user', 'count'])

// 方式2:带类型验证(推荐)
defineProps({
  title: {
    type: String,
    required: true
  },
  user: {
    type: Object,
    default: () => ({})
  },
  count: {
    type: Number,
    default: 0
  }
})

// 方式3:使用 TypeScript(最佳实践)
interface Props {
  title: string
  user: {
    name: string
    age: number
    email: string
  }
  count?: number
}

defineProps<Props>()
</script>

为什么推荐带验证?

它能提前发现传参错误,比如把字符串传给了 count,Vue 会在控制台报错,避免线上bug。


Emits:子传父的事件机制

适用场景:子组件需要通知父组件有事发生,比如表单提交、按钮点击、输入变化等。

<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <button @click="handleButtonClick">通知父组件</button>
    <input 
      :value="inputValue" 
      @input="handleInputChange"
      placeholder="输入内容..."
    />
  </div>
</template>

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

// 定义可触发的事件
const emit = defineEmits(['button-clicked', 'input-changed', 'update:modelValue'])

const inputValue = ref('')

const handleButtonClick = () => {
  // 触发事件并传递数据
  emit('button-clicked', {
    message: '按钮被点击了!',
    timestamp: new Date().toISOString()
  })
}

const handleInputChange = (event) => {
  inputValue.value = event.target.value
  emit('input-changed', inputValue.value)
  
  // 支持 v-model 的更新方式
  emit('update:modelValue', inputValue.value)
}
</script>
<!-- 父组件 Parent.vue -->
<template>
  <div class="parent">
    <ChildComponent 
      @button-clicked="handleChildButtonClick"
      @input-changed="handleChildInputChange"
    />
    
    <div v-if="lastEvent">
      <p>最后收到的事件:{{ lastEvent.type }}</p>
      <p>数据:{{ lastEvent.data }}</p>
    </div>
  </div>
</template>

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

const lastEvent = ref(null)

const handleChildButtonClick = (data) => {
  lastEvent.value = {
    type: 'button-clicked',
    data: data
  }
  console.log('收到子组件消息:', data)
}

const handleChildInputChange = (value) => {
  lastEvent.value = {
    type: 'input-changed',
    data: value
  }
  console.log('输入内容:', value)
}
</script>

关键点:

  • 子组件不直接修改父组件数据,而是发出请求,由父组件决定如何处理。
  • 这种解耦设计让组件更可复用、更易测试。

2. v-model:双向绑定的语法糖

v-model 在 Vue3 中变得更加强大,支持多个 v-model 绑定。

基础用法

<!-- 父组件 -->
<template>
  <div>
    <CustomInput v-model="username" />
    <p>当前用户名:{{ username }}</p>
    
    <!-- 多个 v-model -->
    <UserForm
      v-model:name="userName"
      v-model:email="userEmail"
      v-model:age="userAge"
    />
  </div>
</template>

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

const username = ref('')
const userName = ref('')
const userEmail = ref('')
const userAge = ref(0)
</script>
<!-- 子组件 CustomInput.vue -->
<template>
  <div class="custom-input">
    <label>用户名:</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="input-field"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 子组件 UserForm.vue -->
<template>
  <div class="user-form">
    <div class="form-group">
      <label>姓名:</label>
      <input
        :value="name"
        @input="$emit('update:name', $event.target.value)"
      />
    </div>
    <div class="form-group">
      <label>邮箱:</label>
      <input
        :value="email"
        @input="$emit('update:email', $event.target.value)"
        type="email"
      />
    </div>
    <div class="form-group">
      <label>年龄:</label>
      <input
        :value="age"
        @input="$emit('update:age', parseInt($event.target.value) || 0)"
        type="number"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({
  name: String,
  email: String,
  age: Number
})

defineEmits(['update:name', 'update:email', 'update:age'])
</script>

v-model的核心优势:

  • 语法简洁,减少样板代码
  • 符合双向绑定的直觉
  • 支持多个v-model绑定
  • 类型安全(配合TypeScript)

适用场景:自定义表单控件(如日期选择器、富文本编辑器)需要双向绑定。


3. Ref / 模板引用:直接操作组件

当需要直接访问子组件或 DOM 元素时,模板引用是最佳选择。

<!-- 父组件 -->
<template>
  <div class="parent">
    <ChildComponent ref="childRef" />
    <CustomForm ref="formRef" />
    <video ref="videoRef" controls>
      <source src="./movie.mp4" type="video/mp4">
    </video>
    
    <div class="controls">
      <button @click="focusInput">聚焦输入框</button>
      <button @click="getChildData">获取子组件数据</button>
      <button @click="playVideo">播放视频</button>
      <button @click="validateForm">验证表单</button>
    </div>
  </div>
</template>

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

// 创建引用
const childRef = ref(null)
const formRef = ref(null)
const videoRef = ref(null)

// 确保 DOM 更新后访问
const focusInput = async () => {
  await nextTick()
  childRef.value?.focusInput()
}

const getChildData = () => {
  if (childRef.value) {
    const data = childRef.value.getData()
    console.log('子组件数据:', data)
  }
}

const playVideo = () => {
  videoRef.value?.play()
}

const validateForm = () => {
  formRef.value?.validate()
}

// 组件挂载后访问
onMounted(() => {
  console.log('子组件实例:', childRef.value)
})
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div class="child">
    <input ref="inputEl" type="text" placeholder="请输入..." />
    <p>内部数据:{{ internalData }}</p>
  </div>
</template>

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

const inputEl = ref(null)
const internalData = ref('这是内部数据')

// 暴露给父组件的方法和数据
defineExpose({
  focusInput: () => {
    inputEl.value?.focus()
  },
  getData: () => {
    return {
      internalData: internalData.value,
      timestamp: new Date().toISOString()
    }
  },
  internalData
})
</script>

适用场景:需要调用子组件方法(如弹窗打开)、聚焦输入框、操作原生元素(如 video 播放)。

4. Provide / Inject:跨层级数据传递

解决"prop 逐级传递"问题,实现祖先与后代组件的直接通信。

<!-- 根组件 App.vue -->
<template>
  <div id="app">
    <Header />
    <div class="main-content">
      <Sidebar />
      <ContentArea />
    </div>
    <Footer />
  </div>
</template>

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

// 提供用户信息
const currentUser = ref({
  id: 1,
  name: '张三',
  role: 'admin',
  permissions: ['read', 'write', 'delete']
})

// 提供应用配置
const appConfig = reactive({
  theme: 'dark',
  language: 'zh-CN',
  apiBaseUrl: import.meta.env.VITE_API_URL
})

// 提供方法
const updateUser = (newUserData) => {
  currentUser.value = { ...currentUser.value, ...newUserData }
}

const updateConfig = (key, value) => {
  appConfig[key] = value
}

// 计算属性
const userPermissions = computed(() => currentUser.value.permissions)

// 提供数据和方法
provide('currentUser', currentUser)
provide('appConfig', appConfig)
provide('updateUser', updateUser)
provide('updateConfig', updateConfig)
provide('userPermissions', userPermissions)
</script>
<!-- 深层嵌套的组件 ContentArea.vue -->
<template>
  <div class="content-area">
    <UserProfile />
    <ArticleList />
  </div>
</template>

<script setup>
// 这个组件不需要处理 props,直接渲染子组件
</script>
<!-- 使用注入的组件 UserProfile.vue -->
<template>
  <div class="user-profile">
    <h3>用户信息</h3>
    <div class="profile-card">
      <p>姓名:{{ currentUser.name }}</p>
      <p>角色:{{ currentUser.role }}</p>
      <p>权限:{{ userPermissions.join(', ') }}</p>
      <p>主题:{{ appConfig.theme }}</p>
    </div>
    <button @click="handleUpdateProfile">更新资料</button>
  </div>
</template>

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

// 注入数据和方法
const currentUser = inject('currentUser')
const appConfig = inject('appConfig')
const userPermissions = inject('userPermissions')
const updateUser = inject('updateUser')

const handleUpdateProfile = () => {
  updateUser({
    name: '李四',
    role: 'user'
  })
}
</script>

Provide/Inject的优势

  • 避免Props逐层传递的繁琐
  • 实现跨层级组件通信
  • 提供全局状态和方法的统一管理
  • 提高代码的可维护性

适用场景:当数据需要从顶层组件传递到底层组件,中间隔了好几层(比如主题、用户信息、语言设置)。


5. Pinia:现代化状态管理

对于复杂应用,Pinia 提供了更优秀的状态管理方案。

创建 Store

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

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    isLoggedIn: false,
    token: '',
    permissions: []
  }),
  
  getters: {
    userName: (state) => state.user?.name || '未登录用户',
    isAdmin: (state) => state.user?.role === 'admin',
    hasPermission: (state) => (permission) => 
      state.permissions.includes(permission)
  },
  
  actions: {
    async login(credentials) {
      try {
        // 模拟 API 调用
        const response = await mockLoginApi(credentials)
        
        this.user = response.user
        this.token = response.token
        this.isLoggedIn = true
        this.permissions = response.permissions
        
        // 保存到 localStorage
        localStorage.setItem('token', this.token)
        
        return { success: true }
      } catch (error) {
        console.error('登录失败:', error)
        return { success: false, error: error.message }
      }
    },
    
    logout() {
      this.user = null
      this.token = ''
      this.isLoggedIn = false
      this.permissions = []
      
      localStorage.removeItem('token')
    },
    
    async updateProfile(userData) {
      if (!this.isLoggedIn) {
        throw new Error('请先登录')
      }
      
      this.user = { ...this.user, ...userData }
      // 这里可以调用 API 更新后端数据
    }
  }
})

// 模拟登录 API
const mockLoginApi = (credentials) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        user: {
          id: 1,
          name: credentials.username,
          role: 'admin'
        },
        token: 'mock-jwt-token',
        permissions: ['read', 'write', 'delete']
      })
    }, 1000)
  })
}

在组件中使用 Store

<!-- UserProfile.vue -->
<template>
  <div class="user-profile">
    <div v-if="userStore.isLoggedIn" class="logged-in">
      <h3>欢迎回来,{{ userStore.userName }}!</h3>
      <div class="user-info">
        <p>角色:{{ userStore.user.role }}</p>
        <p>权限:{{ userStore.permissions.join(', ') }}</p>
      </div>
      
      <div class="actions">
        <button 
          @click="updateName" 
          :disabled="!userStore.hasPermission('write')"
        >
          更新姓名
        </button>
        <button @click="userStore.logout" class="logout-btn">
          退出登录
        </button>
      </div>
    </div>
    
    <div v-else class="logged-out">
      <LoginForm />
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import LoginForm from './LoginForm.vue'

const userStore = useUserStore()

const updateName = () => {
  userStore.updateProfile({
    name: `用户${Math.random().toString(36).substr(2, 5)}`
  })
}
</script>
<!-- LoginForm.vue -->
<template>
  <div class="login-form">
    <h3>用户登录</h3>
    <form @submit.prevent="handleLogin">
      <div class="form-group">
        <input 
          v-model="credentials.username" 
          placeholder="用户名"
          required
        />
      </div>
      <div class="form-group">
        <input 
          v-model="credentials.password" 
          type="password" 
          placeholder="密码"
          required
        />
      </div>
      <button type="submit" :disabled="loading">
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </form>
    
    <div v-if="message" class="message" :class="messageType">
      {{ message }}
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const credentials = reactive({
  username: '',
  password: ''
})

const loading = ref(false)
const message = ref('')
const messageType = ref('')

const handleLogin = async () => {
  loading.value = true
  message.value = ''
  
  const result = await userStore.login(credentials)
  
  if (result.success) {
    message.value = '登录成功!'
    messageType.value = 'success'
  } else {
    message.value = `登录失败:${result.error}`
    messageType.value = 'error'
  }
  
  loading.value = false
}
</script>

Pinia 优势:

  • 无 mutations,直接修改 state
  • 完美支持 TypeScript
  • DevTools 调试友好
  • 模块化设计,易于拆分

适用场景:中大型应用,多个组件需要共享复杂状态(如用户登录态、购物车、全局配置)。

6. 事件总线:轻量级全局通信

Vue3 移除了实例上的 onon、off 方法,不再支持这种模式,但我们可以使用 mitt 库实现。

// utils/eventBus.js
import mitt from 'mitt'

// 创建全局事件总线
const eventBus = mitt()

// 定义事件类型
export const EVENTS = {
  USER_LOGIN: 'user:login',
  USER_LOGOUT: 'user:logout',
  NOTIFICATION_SHOW: 'notification:show',
  MODAL_OPEN: 'modal:open',
  THEME_CHANGE: 'theme:change'
}

export default eventBus
<!-- 发布事件的组件 -->
<template>
  <div class="publisher">
    <h3>事件发布者</h3>
    <div class="buttons">
      <button @click="sendNotification">发送通知</button>
      <button @click="openModal">打开模态框</button>
      <button @click="changeTheme">切换主题</button>
    </div>
  </div>
</template>

<script setup>
import eventBus, { EVENTS } from '@/utils/eventBus'

const sendNotification = () => {
  eventBus.emit(EVENTS.NOTIFICATION_SHOW, {
    type: 'success',
    title: '操作成功',
    message: '这是一个来自事件总线的通知',
    duration: 3000
  })
}

const openModal = () => {
  eventBus.emit(EVENTS.MODAL_OPEN, {
    component: 'UserForm',
    props: { userId: 123 },
    title: '用户表单'
  })
}

const changeTheme = () => {
  const themes = ['light', 'dark', 'blue']
  const randomTheme = themes[Math.floor(Math.random() * themes.length)]
  
  eventBus.emit(EVENTS.THEME_CHANGE, {
    theme: randomTheme,
    timestamp: new Date().toISOString()
  })
}
</script>
<!-- 监听事件的组件 -->
<template>
  <div class="listener">
    <h3>事件监听者</h3>
    <div class="events-log">
      <div 
        v-for="(event, index) in events" 
        :key="index"
        class="event-item"
      >
        <strong>{{ event.type }}</strong>
        <span>{{ event.data }}</span>
        <small>{{ event.timestamp }}</small>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus, { EVENTS } from '@/utils/eventBus'

const events = ref([])

// 事件处理函数
const handleNotification = (data) => {
  events.value.unshift({
    type: EVENTS.NOTIFICATION_SHOW,
    data: `通知: ${data.title} - ${data.message}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleModalOpen = (data) => {
  events.value.unshift({
    type: EVENTS.MODAL_OPEN,
    data: `打开模态框: ${data.component}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

const handleThemeChange = (data) => {
  events.value.unshift({
    type: EVENTS.THEME_CHANGE,
    data: `主题切换为: ${data.theme}`,
    timestamp: new Date().toLocaleTimeString()
  })
}

// 注册事件监听
onMounted(() => {
  eventBus.on(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.on(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.on(EVENTS.THEME_CHANGE, handleThemeChange)
})

// 组件卸载时移除监听
onUnmounted(() => {
  eventBus.off(EVENTS.NOTIFICATION_SHOW, handleNotification)
  eventBus.off(EVENTS.MODAL_OPEN, handleModalOpen)
  eventBus.off(EVENTS.THEME_CHANGE, handleThemeChange)
})
</script>

不太推荐使用。为什么?

  • 数据流向不透明,难以追踪
  • 容易忘记 off 导致内存泄漏
  • 大型项目维护困难
  • 建议:优先用 Pinia 或 provide/inject

适用场景:小型项目中,两个无关联组件需要临时通信(如通知弹窗、模态框控制)。


7. 属性透传($attrs)和边界处理

当你封装一个组件,并希望把未声明的属性自动传递给内部元素时,就用 $attrs。

<!-- 基础组件 BaseButton.vue -->
<template>
  <button 
    v-bind="filteredAttrs"
    class="base-button"
    @click="handleClick"
  >
    <slot></slot>
  </button>
</template>

<script setup>
import { computed, useAttrs } from 'vue'

const attrs = useAttrs()

// 过滤掉不需要透传的属性
const filteredAttrs = computed(() => {
  const { class: className, style, ...rest } = attrs
  return rest
})

const emit = defineEmits(['click'])

const handleClick = (event) => {
  emit('click', event)
}

// 也可以选择性地暴露 attrs
defineExpose({
  attrs
})
</script>
</style>
<!-- 使用基础组件 -->
<template>
  <div>
    <!-- 透传 class、style、data-* 等属性 -->
    <BaseButton
      class="custom-btn"
      style="color: red;"
      data-testid="submit-button"
      title="提交按钮"
      @click="handleSubmit"
    >
      提交表单
    </BaseButton>
    
    <!-- 多个按钮使用相同的基组件 -->
    <BaseButton
      class="secondary-btn"
      data-testid="cancel-button"
      @click="handleCancel"
    >
      取消
    </BaseButton>
  </div>
</template>

<script setup>
const handleSubmit = () => {
  console.log('提交表单')
}

const handleCancel = () => {
  console.log('取消操作')
}
</script>

<style>
.custom-btn {
  background: blue;
  color: white;
}

.secondary-btn {
  background: gray;
  color: white;
}
</style>

特性

  • 用户传的 class 和 style 会和组件内部的样式合并(Vue 自动处理)。
  • 所有 data-、title、aria- 等原生 HTML 属性都能正常生效。
  • 你不用提前知道用户会传什么,也能支持!

适用场景:封装通用组件(如按钮、输入框),希望保留原生 HTML 属性(class、style、data-* 等)。

8. 组合式函数:逻辑复用

对于复杂的通信逻辑,可以使用组合式函数封装。

// composables/useCommunication.js
import { ref, onUnmounted } from 'vue'

export function useCommunication() {
  const messages = ref([])
  const listeners = new Map()

  const sendMessage = (type, data) => {
    messages.value.unshift({
      type,
      data,
      timestamp: new Date().toISOString()
    })
    
    // 通知监听者
    if (listeners.has(type)) {
      listeners.get(type).forEach(callback => {
        callback(data)
      })
    }
  }

  const onMessage = (type, callback) => {
    if (!listeners.has(type)) {
      listeners.set(type, new Set())
    }
    listeners.get(type).add(callback)
  }

  const offMessage = (type, callback) => {
    if (listeners.has(type)) {
      listeners.get(type).delete(callback)
    }
  }

  // 清理函数
  const cleanup = () => {
    listeners.clear()
  }

  onUnmounted(cleanup)

  return {
    messages,
    sendMessage,
    onMessage,
    offMessage,
    cleanup
  }
}
<!-- 使用组合式函数 -->
<template>
  <div class="communication-demo">
    <div class="senders">
      <MessageSender />
      <EventSender />
    </div>
    <div class="receivers">
      <MessageReceiver />
      <EventReceiver />
    </div>
    <div class="message-log">
      <h4>消息日志</h4>
      <div 
        v-for="(msg, index) in messages" 
        :key="index"
        class="log-entry"
      >
        [{{ formatTime(msg.timestamp) }}] {{ msg.type }}: {{ msg.data }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { useCommunication } from '@/composables/useCommunication'
import MessageSender from './MessageSender.vue'
import MessageReceiver from './MessageReceiver.vue'
import EventSender from './EventSender.vue'
import EventReceiver from './EventReceiver.vue'

const { messages } = useCommunication()

const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString()
}
</script>

优势

  • 逻辑高度复用
  • 类型安全(配合 TS)
  • 易于单元测试

适用场景:将复杂的通信逻辑抽象成可复用的函数,比如 WebSocket 连接、本地存储同步等。


避坑指南

1. Props 设计原则

// 好的 Props 设计
defineProps({
  // 必需属性
  title: { type: String, required: true },
  
  // 可选属性带默认值
  size: { type: String, default: 'medium' },
  
  // 复杂对象
  user: { 
    type: Object, 
    default: () => ({ name: '', age: 0 }) 
  },
  
  // 验证函数
  count: {
    type: Number,
    validator: (value) => value >= 0 && value <= 100
  }
})

2. 事件命名规范

// 使用 kebab-case 事件名
defineEmits(['update:title', 'search-change', 'form-submit'])

// 避免使用驼峰命名
// defineEmits(['updateTitle']) // 不推荐

3. Provide/Inject 的响应性

// 保持响应性
const data = ref({})
provide('data', readonly(data))

// 提供修改方法
const updateData = (newData) => {
  data.value = { ...data.value, ...newData }
}
provide('updateData', updateData)

4. 内存泄漏预防

// 及时清理事件监听
onUnmounted(() => {
  eventBus.off('some-event', handler)
})

// 清理定时器
const timer = setInterval(() => {}, 1000)
onUnmounted(() => clearInterval(timer))

总结

经过上面的详细讲解,相信大家对 Vue3 的组件通信有了更深入的理解。让我最后做个总结:

  • 核心原则:根据组件关系选择合适方案
  • 父子组件:优先使用 Props/Emits,简单直接
  • 表单控件:v-model是最佳选择,语法优雅
  • 深层嵌套:Provide/Inject 避免 prop 透传地狱
  • 全局状态:Pinia 专业强大,适合复杂应用
  • 临时通信:事件总线可用但需谨慎
  • 组件封装:属性透传提供更好用户体验
  • 逻辑复用:组合式函数提升代码质量

在实际开发中,可以这样:

  1. 先从 Props/Emits 开始,这是基础
  2. 熟练掌握 v-model 的表单处理
  3. 在需要时引入 Pinia,不要过度设计
  4. 保持代码的可读性和可维护性

简单的需求用简单的方案,复杂的需求才需要复杂的工具。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计》

《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》

《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》

《这 10 个 Vue3 性能优化技巧很实用,但很多项目都没用上》

昨天以前首页

Vue3 的设计目标是什么?相比 Vue2 做了哪些关键优化?

作者 刘大华
2025年12月31日 08:45

刚开始用 Vue2 的时候,感觉就像拿到了一把顺手的新工具。简单、直观,做东西也特别快。

但随着项目越来越复杂,项目渐渐的也会遇到一些烦恼。组件的代码越写越长,相关的逻辑分散在data、methods、computed各种不同的地方,想复用一段功能也挺费劲的。同时打包后的文件也越来越大。

后来用了Vue3,可以说是完美的解决了我上面的几个痛点。

下面我们就来看下 vue3 到底做了哪些优化。

Vue 3.0 的四大设计目标

任何一次技术重构都有其核心目标,Vue 3 主要围绕以下四点展开:

  1. 更小:通过 Tree-shaking 等技术,让打包体积比 Vue 2 更小。
  2. 更快:优化虚拟 DOM,提升渲染和更新性能。
  3. 更易维护:采用 TypeScript 重写,代码结构更清晰模块化。
  4. 更好的扩展性:提供组合式 API 等新特性,应对复杂应用场景。

下面我们就来看看这些目标是如何具体实现的。


性能优化:底层引擎的重构

1. 响应式系统:Proxy 替代 Object.defineProperty

这是 Vue3 最核心的改进之一。

Vue2的响应式原理:

// Vue2 使用 Object.defineProperty
const data = {};
Object.defineProperty(data, 'name', {
    get() {
        console.log('读取name');
        return '张三';
    },
    set(newVal) {
        console.log('设置name:', newVal);
    }
});

Vue2 的局限性:

  • 无法检测属性的添加和删除
  • 对数组的支持需要特殊处理
  • 初始化时需要递归遍历整个对象

Vue3 的解决方案:

// Vue 3 使用 Proxy
const data = { name: '张三' };
const proxyData = new Proxy(data, {
    get(target, key) {
        console.log(`读取${key}`);
        return target[key];
    },
    set(target, key, value) {
        console.log(`设置${key}:`, value);
        target[key] = value;
        return true;
    },
    deleteProperty(target, key) {
        console.log(`删除${key}`);
        delete target[key];
        return true;
    }
});

Proxy 的优势:

  • 可以监听动态添加的属性
  • 支持数组索引修改、length 修改
  • 支持 Map、Set 等数据结构
  • 性能更好

2. 编译时优化:模板编译

Vue3 的编译器会分析模板,为动态内容添加标记。

// 模板
<template>
  <div class="header">
    <img src="./logo.png" />
    <h1>{{ title }}</h1>
  </div>
</template>

Vue2:每次渲染都重新创建 <img> 节点(虽然是静态的)。

Vue3:编译时发现 <img> 永远不变,就把它缓存起来,只创建一次!

// 编译后伪代码(简化)
const staticImg = createVNode('img', { src: './logo.png' })

function render() {
  return createVNode('div', { class: 'header' }, [
    staticImg, // 直接复用!
    createTextVNode(ctx.title) // 只有这里动态更新
  ])
}

3. Tree-shaking:按需引入

Vue3 的模块化设计使得未使用的功能不会被打包:

import { createApp, h } from 'vue'; // 只引入需要的API

如果你不使用 transition 组件,它的代码就不会出现在最终打包文件中


开发体验的升级:组合式 API

这是 Vue3 在代码组织方式上的重大改进!

Vue2 Options API 的问题:

export default {
    data() {
        return {
            users: [],
            searchQuery: '',
            loading: false
        }
    },
    methods: {
        fetchUsers() {
            // 获取用户数据
        },
        searchUsers() {
            // 搜索用户
        }
    },
    computed: {
        filteredUsers() {
            // 过滤用户
        }
    },
    mounted() {
        this.fetchUsers();
    }
}

同一个功能(用户管理)的逻辑被拆分到不同的选项中,组件复杂后难以维护。

Vue3 Composition API 的解决方案:

<template>
  <div>
    <input v-model="searchQuery" placeholder="搜索用户">
    <div v-if="loading">加载中...</div>
    <UserList :users="filteredUsers" />
  </div>
</template>

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

// 用户管理功能 - 所有相关逻辑在一起
const users = ref([])
const searchQuery = ref('')
const loading = ref(false)

const filteredUsers = computed(() => {
  return users.value.filter(user => 
    user.name.includes(searchQuery.value)
  )
})

async function fetchUsers() {
  loading.value = true
  try {
    users.value = await api.getUsers()
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchUsers()
})

// 其他功能也可以这样组织...
// const posts = ref([])
// const fetchPosts = async () => { ... }
</script>

vue3 组合式函数:

// composables/useUserManagement.js
import { ref, computed, onMounted } from 'vue'

export function useUserManagement() {
    const users = ref([])
    const searchQuery = ref('')
    const loading = ref(false)

    const filteredUsers = computed(() => {
        return users.value.filter(user => 
            user.name.includes(searchQuery.value)
        )
    })

    async function fetchUsers() {
        loading.value = true
        try {
            users.value = await api.getUsers()
        } finally {
            loading.value = false
        }
    }

    onMounted(fetchUsers)

    return {
        users,
        searchQuery,
        loading,
        filteredUsers,
        fetchUsers
    }
}
<script setup>
// 在组件中使用
import { useUserManagement } from './composables/useUserManagement'

const { 
    users, 
    searchQuery, 
    loading, 
    filteredUsers,
    fetchUsers 
} = useUserManagement()
</script>

Vue3 同时支持 Options API 和 Composition API,你可以根据项目复杂度和个人偏好选择,甚至混合使用。


其他重要新特性

1. Teleport:任意传送

<template>
  <div class="app">
    <button @click="showModal = true">打开弹窗</button>
    
    <!-- 将弹窗内容传送到 body 下 -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <h2>我是弹窗</h2>
        <button @click="showModal = false">关闭</button>
      </div>
    </Teleport>
  </div>
</template>

2. Fragments:多根节点支持

<template>
  <!-- Vue 3 支持多个根节点 -->
  <header>头部</header>
  <main>主要内容</main>
  <footer>底部</footer>
</template>

3. Suspense:异步组件处理

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
// 异步组件
const AsyncComponent = defineAsyncComponent(() => 
  import('./AsyncComponent.vue')
)
</script>

总结

特性对比 Vue 2 Vue 3 优势
响应式系统 Object.defineProperty Proxy 功能更完善,性能更好
代码组织 Options API Composition API 逻辑复用和组织更灵活
TypeScript 需要额外配置 原生支持 开发体验更好
包大小 全量引入 Tree-shaking 打包体积更小
新功能 有限 Teleport、Suspense等 开发能力更强

当然 Vue3 并没有抛弃过去,而是把选择权交给了我们:小项目可以用熟悉的 Options API 快速上手,大项目则可以借助 Composition API 和 TypeScript 更从容的组织代码。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计》

《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》

《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》

《这 10 个 Vue3 性能优化技巧很实用,但很多项目都没用上》

❌
❌