Vue3 规范推荐的 <script setup> 中书写顺序(附:如何响应路由参数变化)

Vue3在<script setup>中推荐按逻辑顺序组织代码:1)导入语句→2)类型定义→3)Props/Emits→4)响应式数据→5)组合式函数→6)计算属性→7)观察器→8)方法→9)生命周期钩子→10)暴露方法。


针对路由参数变化,默认组件不会重新创建但会复用,可通过watch监听route对象或使用key强制刷新。


注意:计算属性返回的是一个包装对象,需要使用.value 访问。


VueRouter4推荐使用onBeforeRouteUpdate钩子处理参数更新,平衡性能与简洁性。


现代生态更推荐结合Pinia和Composition API,或采用VueQuery等数据获取模式。


在 Vue 3 的 <script setup> 中,推荐的代码组织顺序遵循逻辑关注点分离可读性优先的原则。


Vue3规范推荐顺序:

html 复制代码
<script lang="ts" setup>
// 1. 导入语句
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'
import PostList from '../components/PostList.vue'

// 2. 类型定义(TypeScript)
interface Props {
  // props 定义
}

// 3. Props / Emits 定义
const props = defineProps<Props>()
const emit = defineEmits<{
  // emits 定义
}>()

// 4. 响应式数据声明(ref, reactive)
const loading = ref(false)
const error = ref<string | null>(null)

// 5. 组合式函数调用(获取路由、store等)
const route = useRoute()
const router = useRouter()
const store = useStore()

// 6. 计算属性(computed)
const currentId = computed(() => route.params.id)
const column = computed(() => store.getters.getColumnById(currentId.value))
const list = computed(() => store.getters.getPostsByCid(currentId.value))

// 7. 观察器(watch)
watch(currentId, (newId, oldId) => {
  if (newId !== oldId) {
    fetchData()
  }
})

// 8. 方法定义(按逻辑相关性分组)
// 数据获取相关
const fetchData = async () => {
  if (!currentId.value) return
  
  loading.value = true
  error.value = null
  
  try {
    await Promise.all([
      store.dispatch('fetchColumns', currentId.value),
      store.dispatch('fetchPosts', currentId.value)
    ])
  } catch (err) {
    error.value = '加载失败,请稍后重试'
    console.error('加载数据失败:', err)
  } finally {
    loading.value = false
  }
}

// 事件处理相关
const handleRefresh = () => {
  fetchData()
}

// 9. 生命周期钩子
onMounted(() => {
  fetchData()
})

// 10. 暴露给模板的方法(如果有需要)
defineExpose({
  refresh: fetchData
})
</script>

关键原则总结:

  1. 从外到内,从依赖到被依赖:先导入外部依赖,再定义内部逻辑

  2. 响应式数据优先ref/reactive 声明在计算属性和方法之前

  3. 计算属性紧接着响应式数据:它们是对响应式数据的派生

  4. 观察器和副作用在方法之后:因为它们依赖已经定义的方法

  5. 生命周期钩子放在最后:因为它们通常调用前面定义的方法

  6. 相关逻辑放在一起:数据获取、事件处理等按功能分组

特别说明异步数据加载顺序:

在 Vue 3 中,推荐的模式是:

  1. setup 阶段:声明所有响应式数据和计算属性

  2. 生命周期钩子:执行初始化数据加载

  3. 自动响应:依赖 Vue 的响应式系统自动更新视图

这种顺序确保了:

  • 代码清晰可维护

  • 响应式系统正常工作

  • 数据流明确(state → computed → view)

  • 易于调试和测试


Vue3中,如果路由参数改变,整个组件会重新渲染,不需要手动监听路由参数变化?


在 Vue Router 中,当路由参数改变时:

  1. 如果是同一个路由组件,默认会复用组件实例,不会重新创建。

  2. 可以通过 key 属性强制重新创建组件来触发完整的生命周期。


在 Vue 3(配合 Vue Router)中,路由参数改变时,默认情况下组件不会被销毁和重新创建(即不会"整个组件重新渲染") ,而是复用同一个组件实例。因此:

  • 组件的 setup() 不会重新执行
  • 生命周期钩子如 onMounted 也不会再次触发
  • route 对象是响应式的,其属性(如 paramsquery)的变化会触发模板或计算属性中的自动更新

✅ 正确理解:

组件是否重新创建?

  • 不会 ,只要路由匹配的是同一个组件(比如 /user/1/user/2),Vue Router 会复用组件实例以提升性能。

  • 如果你希望组件在参数变化时重新创建,可以给 <router-view> 加上 key:vue

    html 复制代码
    <router-view :key="$route.fullPath" />  

    这样每次路径变化都会强制重新创建组件。


如何响应路由参数变化?

  • 使用 watch + 计算属性 监听 route 或具体参数
    *

    javascript 复制代码
    <template>
      <!-- 不加 key,组件会复用 -->
      <ColumnDetail />
    </template>
    
    <script setup>
    // 这种情况下,组件不会重新创建,需要 watch 监听
    const currentId = computed(() => route.params.id)
    //计算属性返回的是一个包装对象,需要使用.value 访问
    watch(currentId.value, fetchData)
    </script>
  • 使用 key 强制重新渲染(推荐,更简洁)
    *

    javascript 复制代码
    <template>
      <router-view :key="route.fullPath" />
      <!-- 或 -->
      <ColumnDetail :key="route.params.id" />
    </template>
    
    <script setup>
    // 组件会重新创建,自动触发 onMounted
    // 不需要 watch
    </script>
  • 在路由层面处理
    *

    javascript 复制代码
    // router/index.js
    {
      path: '/column/:id',
      component: () => import('../views/ColumnDetail.vue'),
      // Vue Router 4 中,props: true 会导致组件重新创建
      props: true
    }
  • 直接使用(适用于模板中直接使用)
    *

    复制代码
      <template>
        <!-- ✅ 这是可行的,会自动更新 -->
        <h2>ID: {{ route.params.id }}</h2>
      </template>
    
      <script setup>
      import { useRoute } from 'vue-router'
    
      const route = useRoute()
    
      // 直接使用是响应式的
      console.log(route.params.id) // 这是响应式的!
      </script>

为什么有时感觉"整个组件重新渲染"?

  • 如果你在模板中直接用了 route.params.xxx,当参数变化时,模板会局部更新 (因为响应式),但这不等于组件被销毁重建。
    *

    javascript 复制代码
    <template>
      <!-- ✅ 这是可行的,会自动更新 -->
      <h2>ID: {{ route.params.id }}</h2>
    </template>
    
    <script setup>
    const route = useRoute()
    
    //route.params 确实是响应式的
    //但在计算属性中直接使用有风险
    
    // ❌ 这样写计算属性不会自动更新!
    const currentId = route.params.id
    
    // ✅ 正确写法:使用函数或 getter
    const currentId = computed(() => route.params.id)
    
    //在 JavaScript 中直接解构会丢失响应式
    // ❌ 丢失响应式
    const { id } = route.params
    
    // ✅ 保持响应式
    const id = computed(() => route.params.id)
    </script>
  • 只有组件实例被销毁重建时,setup()onMounted 才会重新执行。


特别提醒

注意

  1. 计算属性是一个包装对象 ,需要 .value 访问实际值

  2. 在模板中 Vue 自动解包 ,不需要 .value

  3. 在 JavaScript 中必须使用 .value

  4. 字符串连接时要注意类型

javascript 复制代码
// 错误的方式
console.log("id = " + computedId) // [object Object]

// 正确的方式
console.log("id =", computedId.value) // id = 123
console.log(`id = ${computedId.value}`) // id = 123

// 或查看完整对象
console.log("computedId 对象:", computedId)
console.log("computedId.value:", computedId.value)

📌 总结:

路由参数变化 ≠ 组件重新渲染(重建)

默认是复用组件 ,你需要手动监听 route 的变化 来响应参数更新(如重新获取数据)。

如果确实需要"重新渲染整个组件",请通过 <router-view :key="..."> 强制刷新。


性能比较,以及Vue3推荐的做法


让我们从性能和最佳实践两个角度来分析。


性能对比分析

方案A:使用 watch 监听参数变化

javascript

复制代码
watch(currentId, fetchData)

优点:

  • 组件实例复用,减少组件创建/销毁开销

  • 保持组件内部状态(如用户输入、滚动位置等)

  • 响应更快(不需要重新创建组件)

缺点:

  • 需要手动管理状态清理(如清除定时器、事件监听等)

  • 可能产生副作用(旧数据的残留)

  • 代码复杂度增加

方案B:使用 key 强制重新渲染

vue

复制代码
<ColumnDetail :key="route.params.id" />

优点:

  • 逻辑简单,生命周期完整

  • 自动清理所有状态

  • 不会产生副作用

  • 更符合直觉

缺点:

  • 组件频繁创建/销毁有性能开销

  • 丢失组件内部状态


Vue 3 官方推荐做法

1. 首选:让组件自动处理(Vue Router 最佳实践)

Vue Router 4 推荐使用路由组件的 onBeforeRouteUpdate 钩子:

html 复制代码
<script lang="ts" setup>
import { onMounted } from 'vue'
import { onBeforeRouteUpdate } from 'vue-router'

// 初始加载
onMounted(fetchData)

// 路由参数更新时的处理
onBeforeRouteUpdate(async (to, from) => {
  // 只有 columnId 改变时才重新加载
  if (to.params.id !== from.params.id) {
    await fetchData(to.params.id)
  }
})
</script>

这是 Vue Router 官方推荐的方式,因为它:

  • 平衡了性能和简洁性

  • 提供更细粒度的控制

  • 不需要手动处理组件复用

2. 次选:使用 watch(如果需要复用组件状态)

html 复制代码
<script lang="ts" setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

// 监听特定参数变化
watch(
  () => route.params.id,
  (newId, oldId) => {
    if (newId !== oldId) {
      fetchData(newId)
    }
  },
  { immediate: true } // 立即执行一次,替代 onMounted
)
</script>

3. 最后选择:使用 key(简单场景)

html 复制代码
<template>
  <ColumnDetail :key="route.params.id" />
</template>

实际开发示例:获取某专栏文章列表


html 复制代码
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useStore } from 'vuex'
import PostList from '../components/PostList.vue'

const route = useRoute()
const store = useStore()
const loading = ref(false)

// 计算属性
const currentId = computed(() => route.params.id)
const column = computed(() => store.getters.getColumnById(currentId.value))
const list = computed(() => store.getters.getPostsByCid(currentId.value))

// 数据加载方法
const loadData = async (id?: string | string[]) => {
  const targetId = id || currentId.value
  if (!targetId) return
  
  loading.value = true
  try {
    await Promise.all([
      store.dispatch('fetchColumns', targetId),
      store.dispatch('fetchPosts', targetId)
    ])
  } finally {
    loading.value = false
  }
}

// 初始加载
loadData()

// 路由参数更新时的处理
onBeforeRouteUpdate(async (to) => {
  await loadData(to.params.id)
})
</script>

或者


html 复制代码
<script lang="ts" setup>
import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const store = useStore()

// ✅ 保持响应式
const currentId = computed(() => route.params.id)

// ✅ 计算属性自动更新
const column = computed(() => store.getters.getColumnById(currentId.value))
const list = computed(() => store.getters.getPostsByCid(currentId.value))

const fetchData = async () => {
  const id = currentId.value
  if (!id) return
  
  await Promise.all([
    store.dispatch('fetchColumns', id),
    store.dispatch('fetchPosts', id)
  ])
}

// ✅ 监听参数变化
watch(currentId, fetchData)

// 初始加载
onMounted(fetchData)
</script>

总结:

  1. 官方推荐:Vue Router 团队维护,兼容性最好

  2. 性能平衡:避免不必要的组件重新创建

  3. 代码清晰:明确区分初始加载和参数更新

  4. 维护性好:逻辑集中,易于理解和调试

何时选择其他方案:

  • 当组件有复杂内部状态需要保持 → 使用 watch

  • 当需要确保每次完全刷新 → 使用 key

  • 当使用现代状态管理库 → 遵循该库的最佳实践


性能建议表格

场景 推荐方案 理由
简单数据展示 onBeforeRouteUpdate Vue Router 官方推荐,平衡性能与简洁
保持复杂组件状态 watch + 状态管理 避免重新初始化复杂状态
每次都需要完全刷新 key 确保数据完全同步
大数据量/频繁切换 watch 减少组件创建开销
SEO/首屏渲染 key 确保服务端渲染一致性

现代 Vue 3 生态的推荐

在最新的 Vue 生态中,更推荐使用:

1. Pinia + Composition API

TypeScript 复制代码
// useColumnStore.ts
export const useColumnStore = defineStore('column', () => {
  const column = ref<Column | null>(null)
  const posts = ref<Post[]>([])
  const loading = ref(false)
  
  const fetchData = async (id: string) => {
    // 数据获取逻辑
  }
  
  return { column, posts, loading, fetchData }
})

// ColumnDetail.vue
const route = useRoute()
const store = useColumnStore()

// 使用 watchEffect 自动响应
watchEffect(async () => {
  if (route.params.id) {
    await store.fetchData(route.params.id as string)
  }
})

2. Vue Query / SWR 模式

TypeScript 复制代码
// 使用 TanStack Query(原 Vue Query)
const { data: column, isLoading } = useQuery({
  queryKey: ['column', route.params.id],
  queryFn: () => fetchColumn(route.params.id as string),
})
相关推荐
web打印社区3 小时前
前端实现浏览器预览打印:从原生方案到专业工具
前端·javascript·vue.js·electron
jiayong233 小时前
Vue2 与 Vue3 生态系统及工程化对比 - 面试宝典
vue.js·面试·职场和发展
徐同保3 小时前
vue.config.ts配置代理解决跨域,配置开发环境开启source-map
前端·javascript·vue.js
Hexene...4 小时前
【前端Vue】npm install时根据新的状态重新引入实际用到的包,不引入未使用到的
前端·vue.js·npm
2301_780669864 小时前
Vue(入门配置、常用指令)、Ajax、Axios
前端·vue.js·ajax·javaweb
我是ed.4 小时前
Vue3 音频标注插件 wavesurfer
前端·vue.js·音视频
Hexene...4 小时前
【前端Vue】出现elementui的index.css引入报错如何解决?
前端·javascript·vue.js·elementui
红色的小鳄鱼4 小时前
Vue 监视属性 (watch) 超全解析:Vue2 Vue3
前端·javascript·css·vue.js·前端框架·html5
web小白成长日记4 小时前
Vue-实例从 createApp 到真实 DOM 的挂载全历程
前端·javascript·vue.js