useEffect 为什么在组件首次渲染时就执行了?
我刚学 React,看到 useEffect 默认会在组件挂载后执行一次,但我不太理解为什么它不等依赖变化才运行。比如我在 Vue 里用 watch 是不会一进来就触发的,但在 React 里写了个空依赖数组,结果还是执行了,这正常吗?
下面是我写的 Vue 对比代码,想搞清楚两者的逻辑差异:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 这个 watch 不会立即执行
watch(count, (newVal) => {
console.log('count changed:', newVal)
})
</script>
首先,useEffect 的第一个参数是一个函数,这个函数会在依赖项数组发生变化时执行。如果你传递一个空数组作为第二个参数,那么 useEffect 就会在组件挂载时执行一次,并且之后不再执行,除非组件卸载再重新挂载。
这种行为背后的原因是为了确保副作用(side effects)能够在组件挂载后立即执行,这对于很多场景来说是非常必要的。比如数据获取、订阅事件等操作,我们希望这些操作在组件显示出来后立刻进行。
在你的例子中,Vue 的 watch 默认不会在初始绑定时触发回调,只有当被监视的数据发生变化时才会执行。这是因为 Vue 的 watch 更倾向于只关注数据的变化,而不是组件的生命周期。
如果你希望 useEffect 在首次渲染时不执行,可以考虑使用一个布尔值标志来控制:
在这个例子中,我们引入了一个 isInitialMount 的状态来跟踪组件是否是首次渲染。如果是首次渲染,我们就直接返回,不执行副作用。这样就能达到类似于 Vue 的 watch 的效果。
需要注意的是,这种方式可能会让你的代码稍微复杂一些,所以在使用之前要权衡一下利弊。有时候,让 useEffect 在首次渲染时执行也是合理的,取决于你的具体需求。
先说结论,useEffect 的设计初衷就是"渲染后执行副作用",它的语义是"当组件渲染完成之后,我要做点什么",而不是"当数据变化时我要做点什么"。这两者看起来很像,但出发点完全不同。
React 的 useEffect 执行逻辑是这样的:组件第一次渲染完成后执行一次,之后每次依赖项变化导致重新渲染后,也会执行。你传了空数组
[]作为依赖,意思是"我不依赖任何 props 或 state",所以后续渲染不会再触发这个 effect,但第一次挂载后的那次执行是雷打不动的。Vue 的 watch 默认是惰性的,它只关注"变化"这个动作,没有变化就不触发。而 useEffect 关注的是"渲染完成"这个时间点,渲染完成了就要执行。
来看一段代码对比两者的差异:
你会发现 Vue 也有
immediate: true选项,开启后行为就跟 React 的 useEffect 一样了。这说明两边都意识到了"立即执行"这个场景的必要性,只是默认值选得不一样。为什么 React 要这样设计?因为 React 团队认为,很多副作用的逻辑结构是这样的:执行某些操作,然后在组件卸载或下次执行前清理。比如订阅事件、开启定时器、发起网络请求。这些操作在首次渲染后就需要执行,而不是等到数据变化。
一个典型的例子:
如果 useEffect 不在首次渲染后执行,这个定时器就不会启动,你还得另外找个地方去初始化它,逻辑就散了。
那如果你真的想要"只在数据变化时执行,首次不触发"的行为呢?可以用 useRef 配合一个标志位来实现:
或者封装成一个自定义 hook,网上有现成的库比如
useUpdateEffect,专门干这事儿。需要注意一点,不要把 useEffect 理解成 Vue 的 watch。useEffect 是声明式的,你告诉 React"渲染后我要做这些事",React 会保证在正确的时机调用。而 watch 是响应式的,你告诉 Vue"这个数据变了你要做什么"。思维模式不一样,用 useEffect 去模拟 watch 的行为,写出来的代码往往很别扭。
总结一下:useEffect 首次执行是设计如此,空数组只是控制"后续是否因依赖变化而再次执行",控制不了首次。习惯了就好,React 这套设计用多了你会发现它在处理副作用时其实挺优雅的,只是跟 Vue 的脑回路不太一样。