Vue 3 Composition API + script setup:写 Vue 像写 React 但更轻

起因

团队主要写 React,但接手一个 Vue 2 老项目。Vue 2 的 Options API(data/methods/
computed 分块)跟 React hooks 风格完全不一样,写起来不顺手。
升级到 Vue 3 + Composition API 后语义跟 React 接近多了,迁移
成本下降。下面记录我的"React 视角看 Vue 3"。

script setup 语法

Vue 3.2+ 的 <script setup> 是 Composition API 的语法糖:

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}

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

onMounted(() => {
  console.log('mounted')
})
</script>

<template>
  <button @click="increment">{{ count }} (doubled: {{ doubled }})</button>
</template>

对比 React:

const [count, setCount] = useState(0)
const doubled = useMemo(() => count * 2, [count])

useEffect(() => {
  console.log(`count: ${count}`)
}, [count])

useEffect(() => {
  console.log('mounted')
}, [])

return <button onClick={() => setCount(c => c + 1)}>{count}</button>

Vue 的 ref(0) ≈ React 的 useState。访问值要 .value(template 里
自动 unwrap)。computeduseMemowatchuseEffect with deps。

ref vs reactive

import { ref, reactive } from 'vue'

const counter = ref(0)
console.log(counter.value)
counter.value = 10

const state = reactive({ count: 0, name: '' })
console.log(state.count)
state.count = 10

ref 适合基础类型;reactive 适合对象。
重要:reactive 不能解构(会失去响应性):

const state = reactive({ count: 0 })
const { count } = state    // ❌ count 不再响应

要解构必须 toRefs

import { toRefs } from 'vue'
const { count } = toRefs(state)   // ✅

为了避免这个坑,我直接全用 ref,复杂状态用 ref({ count: 0, name: '' })

组合函数(custom hook 等价物)

抽象逻辑成 useXxx

// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const doubled = computed(() => count.value * 2)
  const inc = () => count.value++
  const dec = () => count.value--
  return { count, doubled, inc, dec }
}

用:

<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubled, inc } = useCounter(10)
</script>

完全等价于 React custom hook。

defineProps / defineEmits(类型化 props / events)

<script setup lang="ts">
const props = defineProps<{
  user: { id: string; name: string }
  count?: number
}>()

const emit = defineEmits<{
  (e: 'increment', value: number): void
  (e: 'reset'): void
}>()

function bump() {
  emit('increment', (props.count ?? 0) + 1)
}
</script>

<template>
  <button @click="bump">{{ user.name }}: {{ count }}</button>
</template>

父组件:

<MyButton :user="me" :count="5" @increment="handleInc" @reset="handleReset" />

类型完全跟着传。

v-model 双向绑定

React 强迫你手写 value={x} onChange={...}。Vue 的 v-model 是糖:

<!-- 父 -->
<MyInput v-model="email" />

<!-- 子 -->
<script setup>
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()
</script>
<template>
  <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)">
</template>

简化版用 defineModel(Vue 3.4+):

<script setup>
const email = defineModel<string>()
</script>
<template>
  <input v-model="email">
</template>

3 行干完。React 里至少 8-10 行。

Pinia:Vue 的状态管理

npm i pinia
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => user.value !== null)

  async function login(email: string, pw: string) {
    const r = await fetch('/api/login', { method: 'POST', body: ... })
    user.value = await r.json()
  }

  function logout() {
    user.value = null
  }

  return { user, isLoggedIn, login, logout }
})
<script setup>
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()

function onSubmit() {
  auth.login(email.value, password.value)
}
</script>

跟 Zustand 类似(但写法是 composition function 而非 set/get)。

路由

npm i vue-router@4
// router.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') },
    { path: '/posts/:id', component: () => import('./views/Post.vue') },
  ],
})

export default router
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()       // /posts/42 → route.params.id = '42'
const router = useRouter()
function goHome() { router.push('/') }
</script>

完全镜像 React Router 的 useParams / useNavigate。

SSR / SSG: Nuxt

类似 React 的 Next.js,Vue 的元框架是 Nuxt:

npx nuxi@latest init my-app

文件系统路由、自动 import、SSR、API routes 等都是 Nuxt 的事,开箱即用。

Vue 比 React 强的几点

  1. template + script + style 三段式:scoped CSS 内置,
    <style scoped> 自动加 hash,组件样式不互相污染
  2. 响应式不需要写依赖数组watch 自动追踪。React useEffect
    写错 deps 是常见 bug。
  3. v-for, v-if, v-show:模板里写就够,不像 JSX 强 .map() / &&
  4. defineModel 简化双向绑定

React 比 Vue 强的几点

  1. 更大生态:Next.js、Remix、库选择更多
  2. TypeScript 更原生:Vue 也支持但偶尔有些边角不丝滑
  3. 更广泛的工作机会

效果

我个人混着写下来:

  • Vue 项目代码量比同等 React 少 20-30%
  • 模板比 JSX 更适合"以视图为中心"的页面(marketing / 表单密集)
  • 复杂状态 / 复杂交互 React 生态成熟度高
  • 个人选择:marketing / 后台管理 → Vue + Nuxt;
    复杂 web app → React

踩过的坑

  1. reactive 解构丢响应性:上面说过。新人最容易踩。

  2. template 里 ref 自动 unwrap
    ```vue

    const count = ref(0)

{{ count }}
`` 习惯之后没事,刚转过来时反复{{ count.value }}多写.value`。

  1. v-html 直接 innerHTML:XSS 隐患。任何用户输入永远不用 v-html。

  2. Vue 3 跟 Vue 2 写法不通用:老 Vue 2 项目升 3 是 breaking change,
    不是 React 16 → 17 那种平滑升级。

  3. Pinia store 在 setup() 外用要 createPinia 先注入
    main.ts 必须 app.use(createPinia()),否则报 "no active pinia"。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。