首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Vue 3 和 React 的第五个差异:生命周期和副作用,决定了你怎么和外部世界打交道

Vue 3 和 React 的第五个差异:生命周期和副作用,决定了你怎么和外部世界打交道

原创
作者头像
peace-free
发布2026-06-16 14:54:45
发布2026-06-16 14:54:45
740
举报
文章被收录于专栏:Vue3Vue3

前端组件不只是把数据渲染成页面。真实项目里,组件还要请求接口、监听窗口变化、操作第三方图表、注册定时器、订阅 WebSocket、同步标题、读写本地缓存。这些事情都不是纯渲染,它们属于副作用。

Vue 3 和 React 都有处理副作用的方式,但心智模型明显不同。Vue 继续保留了比较明确的生命周期钩子,例如 onMountedonUpdatedonUnmounted,同时提供 watchwatchEffect 处理响应式变化。React 则主要依赖 useEffectuseLayoutEffect、事件处理函数和 ref,把副作用放在渲染之后同步。

很多项目的 bug,不是写模板或 JSX 写错,而是副作用放错了地方。理解这部分差异,比记住几个 API 名字重要得多。

image-20260516102556083
image-20260516102556083

一、Vue 的生命周期:阶段感很明确

Vue 3 的生命周期钩子很直观。组件挂载后做什么,更新后做什么,卸载前做什么,都有对应 API。比如:

代码语言:vue
复制
<script setup>
import { onMounted, onUnmounted } from 'vue'

function resize() {
  console.log(window.innerWidth)
}

onMounted(() => {
  window.addEventListener('resize', resize)
})

onUnmounted(() => {
  window.removeEventListener('resize', resize)
})
</script>

这段代码表达很清楚:组件出现在页面上之后监听窗口变化,组件销毁时清理监听。对习惯传统生命周期的人来说,Vue 的写法容易理解。

Vue 3 的 Composition API 让生命周期可以写在组合式函数里。比如多个组件都要监听窗口宽度,可以封装成 useWindowSize(),内部自己注册 onMountedonUnmounted。调用方只拿到响应式数据,不必关心监听和清理细节。

这让 Vue 的副作用组织方式比较接近“创建资源、注册生命周期、返回响应式结果”。组件更新不是反复执行 setup,所以生命周期注册也不会因为每次渲染而重复进入。这一点和 React 很不一样。

二、React 的 effect:不是生命周期的简单替代品

React 里很多人会把 useEffect 理解成 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合。这种理解能帮你入门,但不够准确。

React 官方文档更推荐把 effect 理解为“把组件和外部系统同步”。组件渲染完成之后,React 执行 effect。effect 可以返回一个清理函数,在依赖变化前或组件卸载时执行。

典型写法是:

代码语言:jsx
复制
import { useEffect } from 'react'

function WindowListener() {
  useEffect(() => {
    function resize() {
      console.log(window.innerWidth)
    }

    window.addEventListener('resize', resize)
    return () => window.removeEventListener('resize', resize)
  }, [])

  return null
}

这段代码看起来和 Vue 的 mounted/unmounted 很像。但 React 的关键点是依赖数组。依赖数组决定 effect 什么时候重新执行。没有依赖数组,每次渲染后都执行;空数组通常表示只在挂载后执行一次并在卸载时清理;有依赖项时,依赖变化后清理旧 effect,再执行新 effect。

比如:

代码语言:jsx
复制
useEffect(() => {
  const connection = createConnection(roomId)
  connection.connect()

  return () => connection.disconnect()
}, [roomId])

这里不是“组件更新后做点什么”这么简单,而是“当前组件需要和 roomId 对应的连接保持同步”。roomId 变了,就断开旧连接,建立新连接。

这个模型很强,但也要求开发者认真处理依赖。漏依赖会造成旧值问题,多依赖或不稳定依赖又会造成重复执行。

三、watch 和 useEffect:都能响应变化,但语义不同

Vue 里处理响应式变化,最常用的是 watchwatchEffect

watch 适合明确监听某个来源:

代码语言:js
复制
watch(userId, async id => {
  user.value = await fetchUser(id)
})

watchEffect 会自动追踪函数里读取到的响应式依赖:

代码语言:js
复制
watchEffect(() => {
  console.log(user.value.name)
})

React 里类似场景通常用 useEffect

代码语言:jsx
复制
useEffect(() => {
  fetchUser(userId).then(setUser)
}, [userId])

表面上都是“某个值变了就做事”。但 Vue 的 watch 是响应式系统的一部分,监听的是响应式 source;React 的 useEffect 是渲染之后的同步机制,依赖来自这一轮渲染里的值。

这会影响你如何思考代码。Vue 中你会问:“这个 watch 依赖哪个 ref 或 reactive 字段?”React 中你会问:“这个 effect 使用了哪些 props、state 或组件内变量?依赖数组是否覆盖了它们?”

Vue 的自动依赖追踪减少了样板,但也要求你理解响应式读取。React 的依赖数组暴露了依赖关系,但也增加了维护成本。

四、请求接口:两边都要防止竞态

副作用里最常见的是请求接口。很多人以为 Vue 和 React 的区别在 API,实际更容易出问题的是竞态。

比如用户连续切换 userId,第一个请求慢,第二个请求快。如果不处理,慢请求后返回,可能覆盖快请求结果。这个问题和框架无关,Vue 和 React 都会遇到。

React 里常见处理方式:

代码语言:jsx
复制
useEffect(() => {
  let ignore = false

  async function load() {
    const result = await fetchUser(userId)
    if (!ignore) setUser(result)
  }

  load()
  return () => {
    ignore = true
  }
}, [userId])

Vue 里可以用 watch 的清理能力:

代码语言:js
复制
watch(userId, async (id, oldId, onCleanup) => {
  let expired = false

  onCleanup(() => {
    expired = true
  })

  const result = await fetchUser(id)
  if (!expired) user.value = result
})

更现代的方式还可以结合 AbortController 取消请求。重点是,不要以为框架会自动处理所有异步竞态。生命周期和 effect 只是给你放置副作用的位置,不会替你定义业务正确性。

五、DOM 操作和第三方库:Vue 更像挂载后接管,React 更强调 ref 同步

在 Vue 里,如果要初始化图表、地图、编辑器,常见方式是在模板中放一个容器 ref,然后 onMounted 初始化:

代码语言:vue
复制
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const el = ref(null)
let chart

onMounted(() => {
  chart = createChart(el.value)
})

onUnmounted(() => {
  chart?.destroy()
})
</script>

<template>
  <div ref="el"></div>
</template>

React 里也类似,用 useRef 获取 DOM,再在 effect 里初始化:

代码语言:jsx
复制
function Chart() {
  const elRef = useRef(null)

  useEffect(() => {
    const chart = createChart(elRef.current)
    return () => chart.destroy()
  }, [])

  return <div ref={elRef} />
}

区别不在能不能做,而在周边心智。Vue 的 onMounted 给你明确的 DOM 可用阶段。React 的 ref 在 commit 阶段赋值,effect 在浏览器绘制之后或布局 effect 阶段执行。多数第三方库用 useEffect 就够了,如果需要在浏览器绘制前同步测量布局,React 可能需要 useLayoutEffect

Vue 也有 nextTick,用于等待 DOM 更新完成后再读取 DOM。React 也有自己的更新批处理和 flush 机制,但日常不应该把 React 写成到处手动操作 DOM。两边都一样:能通过声明式渲染解决的,不要优先选择直接改 DOM。

六、严格模式和开发环境行为:React 更容易让新手困惑

React 在开发环境的 Strict Mode 下,会有意重复调用一些函数或重新执行 effect 的 setup/cleanup,用来帮助发现不纯渲染和缺少清理的问题。很多初学者看到接口请求执行两次,会误以为 React 有 bug。实际上这是开发模式下的检查行为,生产环境不会以同样方式重复。

Vue 也有开发环境警告,也会提示不合理写法,但在副作用重复执行这件事上,React Strict Mode 更容易让新人困惑。

这不是坏事。React 通过这种方式逼你写出可清理、可重复执行的 effect。如果 effect 写得正确,重复 setup 和 cleanup 不应该破坏业务。真正的问题通常是 effect 里做了不可重复的事情,或者忘记清理订阅。

七、实践建议

Vue 项目里,不要把所有事情都塞进 onMounted。如果某个逻辑依赖响应式值变化,优先用 watchwatchEffect。如果是资源创建和销毁,用生命周期钩子。组合式函数内部有副作用时,要清晰命名,并在卸载时清理。能用 computed 表达的数据派生,不要用 watch 手动同步。

React 项目里,不要把 useEffect 当成万能工具。事件触发的业务逻辑应该放在事件处理函数里,而不是先改状态再等 effect 捕捉。能够在渲染期间计算出来的值,不要放 effect 里再 setState。effect 主要用来同步外部系统,例如网络请求、订阅、DOM 插件、浏览器 API。依赖数组不要靠感觉写,遵守 lint 规则更稳。

八、结论:Vue 用生命周期和响应式监听分工,React 用 effect 同步外部系统

Vue 3 的副作用模型更分层:生命周期处理组件阶段,watch 处理响应式变化,computed 处理派生数据。React 的副作用模型更集中:组件渲染产生 UI,effect 在渲染之后把外部系统同步到当前状态。

Vue 的优势是阶段清楚、和响应式系统衔接自然。React 的优势是模型统一、表达能力强,但依赖数组和闭包需要认真对待。

写副作用时,不要只问“这个 API 怎么用”。更应该问:这个逻辑是事件导致的,还是渲染后同步外部系统?是资源生命周期,还是响应式值变化?需要清理吗?会不会有异步竞态?这些问题想清楚,Vue 和 React 都能写得稳定。

代码补充:副作用清理和请求取消

真实项目里,请求取消比“发起请求”更重要。Vue 可以在 watch 里使用清理函数配合 AbortController

代码语言:js
复制
import { ref, watch } from 'vue'

const userId = ref('1001')
const user = ref(null)
const error = ref(null)

watch(userId, async (id, oldId, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())

  try {
    error.value = null
    const res = await fetch(`/api/users/${id}`, {
      signal: controller.signal
    })
    user.value = await res.json()
  } catch (e) {
    if (e.name !== 'AbortError') error.value = e
  }
}, { immediate: true })

React 中同样可以在 effect 清理函数里取消旧请求:

代码语言:jsx
复制
function UserDetail({ userId }) {
  const [user, setUser] = useState(null)
  const [error, setError] = useState(null)

  useEffect(() => {
    const controller = new AbortController()

    async function load() {
      try {
        setError(null)
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        })
        setUser(await res.json())
      } catch (e) {
        if (e.name !== 'AbortError') setError(e)
      }
    }

    load()
    return () => controller.abort()
  }, [userId])

  if (error) return <p>加载失败</p>
  if (!user) return <p>加载中...</p>
  return <h1>{user.name}</h1>
}

如果是事件触发的行为,不要硬塞进 effect。React 里点击保存更适合直接放在事件处理函数:

代码语言:jsx
复制
async function handleSubmit() {
  setSaving(true)
  try {
    await saveForm(form)
  } finally {
    setSaving(false)
  }
}

Vue 里也是同理:

代码语言:js
复制
async function submit() {
  saving.value = true
  try {
    await saveForm(form)
  } finally {
    saving.value = false
  }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Vue 的生命周期:阶段感很明确
  • 二、React 的 effect:不是生命周期的简单替代品
  • 三、watch 和 useEffect:都能响应变化,但语义不同
  • 四、请求接口:两边都要防止竞态
  • 五、DOM 操作和第三方库:Vue 更像挂载后接管,React 更强调 ref 同步
  • 六、严格模式和开发环境行为:React 更容易让新手困惑
  • 七、实践建议
  • 八、结论:Vue 用生命周期和响应式监听分工,React 用 effect 同步外部系统
  • 代码补充:副作用清理和请求取消
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档