阅读视图

发现新文章,点击刷新页面。

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

刚开始用 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 性能优化技巧很实用,但很多项目都没用上》

async/await 到底要不要加 try-catch?异步错误处理最佳实践

上周五下午,我正准备下班,产品经理突然跑过来:“用户反馈说提交订单后没反应,是不是又出 bug 了?”

我一查日志,发现接口报了 500 错误,但页面上什么提示都没有。

原来是我写异步请求时忘了加try-catch。用户点完提交就以为成功了,结果订单根本没生成。

那一刻的我才意识到:async/await 错误处理真的不能省。


先来理解 async/await 是什么

简单来说,async/await是处理异步操作的语法糖,让异步代码看起来像同步代码一样直观。

没有 async/await 的时代:

// 回调地狱
fetchData(function(result1) {
  fetchMoreData(result1, function(result2) {
    fetchEvenMoreData(result2, function(result3) {
      // 更多嵌套...
    })
  })
})

有了 async/await 之后:

// 同步般的写法
async function getData() {
  const result1 = await fetchData()
  const result2 = await fetchMoreData(result1)
  const result3 = await fetchEvenMoreData(result2)
  return result3
}

是不是清爽多了?但是,如果 await 后面的 Promise 发生错误(比如网络请求失败),这个错误会直接抛出,如果不捕获,就会导致程序崩溃。


什么时候必须加 try-catch?

1. 需要给用户明确反馈的场景

举个例子:用户点击提交按钮,如果失败了却没有任何提示,用户会以为提交成功,这体验多差啊!

<template>
  <div>
    <button @click="submitOrder" :disabled="loading">
      {{ loading ? '提交中...' : '提交订单' }}
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      successMessage: '',
      errorMessage: ''
    }
  },
  methods: {
    async submitOrder() {
      // 开始加载
      this.loading = true
      this.errorMessage = ''
      
      try {
        // 尝试提交订单
        const result = await this.$http.post('/api/orders', this.orderData)
        
        // 提交成功
        this.successMessage = '订单提交成功!'
        this.$router.push('/success') // 跳转到成功页面
        
      } catch (error) {
        // 根据不同错误类型给用户不同的提示
        if (error.response?.status === 401) {
          this.errorMessage = '请先登录后再提交订单'
        } else if (error.response?.status === 400) {
          this.errorMessage = '订单数据有误,请检查后重试'
        } else if (error.response?.status === 500) {
          this.errorMessage = '服务器繁忙,请稍后重试'
        } else {
          this.errorMessage = '网络错误,请检查网络连接'
        }
      } finally {
        // 无论成功失败,都要取消加载状态
        this.loading = false
      }
    }
  }
}
</script>

关键点:

  • 用户操作必须有反馈
  • 不同错误给出不同提示
  • 使用 finally 确保加载状态正确重置

2. 需要继续执行后续逻辑的场景

有时候,即使某个请求失败了,我们仍然希望继续执行其他操作。

async function initializePage() {
  // 获取用户基本信息(重要)
  try {
    this.userInfo = await this.$http.get('/api/user/info')
  } catch (error) {
    console.error('获取用户信息失败,但页面仍可正常使用')
    // 即使失败,也继续执行下面的逻辑
  }
  
  // 获取用户设置(重要)
  try {
    this.userSettings = await this.$http.get('/api/user/settings')
  } catch (error) {
    console.error('获取用户设置失败')
    // 使用默认设置继续
    this.userSettings = this.defaultSettings
  }
  
  // 获取推荐内容(非关键,失败也没关系)
  try {
    this.recommendations = await this.$http.get('/api/recommendations')
  } catch (error) {
    // 静默失败,不影响主要功能
    console.warn('推荐内容加载失败')
  }
}

什么时候可以不加 try-catch?

1. 有全局错误拦截器的情况

如果你的项目配置了全局的 HTTP 拦截器,那么很多错误已经被统一处理了。

// http.js - 全局拦截器
this.$http.interceptors.response.use(
  response => response,
  error => {
    // 全局统一处理错误
    if (error.response?.status === 401) {
      router.push('/login')
    } else if (error.response?.status >= 500) {
      Message.error('服务器错误,请稍后重试')
    }
    return Promise.reject(error)
  }
)

// 组件中 - 不需要重复处理
async fetchData() {
  // 错误已经被全局拦截器处理了
  const data = await this.$http.get('/api/data')
  this.list = data
}

2. 错误需要向上抛出的情况

在编写可复用的函数时,通常不应该在函数内部处理错误,而是让调用方来决定如何处理。

// api/user.js - 用户相关的 API 函数
export const userApi = {
  // 不处理错误,让调用方决定如何处理
  async getUserProfile(userId) {
    const response = await this.$http.get(`/api/users/${userId}`)
    return response.data
  },
  
  async updateUserProfile(userId, profile) {
    const response = await this.$http.put(`/api/users/${userId}`, profile)
    return response.data
  }
}

// 组件中 - 调用方处理错误
export default {
  methods: {
    async loadUserProfile() {
      try {
        this.profile = await userApi.getUserProfile(this.userId)
      } catch (error) {
        this.$message.error('加载用户信息失败')
      }
    }
  }
}

让代码更简洁

每次都写 try-catch 确实有点啰嗦。我们可以封装一个工具函数:

// utils/safeAsync.js
export function safeAsync(promise) {
  return promise
    .then(data => [null, data])      // 成功:[null, 数据]
    .catch(err => [err, null])       // 失败:[错误, null]
}

使用方式:

import { safeAsync } from '@/utils/safeAsync'

async function loadUser() {
  const [err, data] = await safeAsync(getUserInfo())
  
  if (err) {
    ElMessage.error('加载失败')
    return
  }
  
  user.value = data
}

这看起来更舒服了吧?这种写法来自 Go 语言的错误优先风格,在 JS 社区也很流行。


多个请求怎么办?先别用 Promise.all

很多朋友喜欢这样写:

// 危险!一个失败,全部失败
const [user, orders] = await Promise.all([
  getUser(),
  getOrders()
])

但如果 getOrders() 挂了,getUser() 的结果也会丢掉!

正确做法:用 Promise.allSettled

const results = await Promise.allSettled([getUser(), getOrders()])

const user = results[0].status === 'fulfilled' ? results[0].value : null
const orders = results[1].status === 'fulfilled' ? results[1].value : []

// 即使订单加载失败,用户信息还能显示!

或者用我们上面的 safeAsync

const [userErr, user] = await safeAsync(getUser())
const [orderErr, orders] = await safeAsync(getOrders())

if (userErr) ElMessage.warning('用户信息加载失败')
if (orderErr) ElMessage.warning('订单加载失败')

全局错误兜底

即使你写了 try-catch,也可能漏掉。所以可以做一些兜底的操作。

比如在全局拦截器处理,或者也可以在 main.js 这样写加个安全网:

// main.js
const app = createApp(App)

// 全局 Vue 错误处理器
app.config.errorHandler = (err, instance, info) => {
  console.error('Vue 组件错误:', err, info)
  ElNotification.error({
    title: '系统异常',
    message: '页面出现错误,请刷新重试'
  })
}

app.mount('#app')

这样,就算你忘了加 try-catch,也不会让用户看到白屏!


总结

  1. 用户需要反馈时必须加 try-catch
  2. 关键业务流程必须加 try-catch
  3. 有全局处理时可以不加,避免重复
  4. 编写可复用函数时通常不加,让调用方处理
  5. 非关键操作可以不加,或简单处理

错误处理不是一刀切的事情,需要根据具体业务场景来决定。好的错误处理能让你的应用更加健壮,用户体验更好。

感谢观看,希望这篇文章能帮你理清思路!

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

📌往期精彩

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

《Java 开发必看:什么时候用 for,什么时候用 Stream?》

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

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

❌