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 的设计初衷就是"渲染后执行副作用",它的语义是"当组件渲染完成之后,我要做点什么",而不是"当数据变化时我要做点什么"。这两者看起来很像,但出发点完全不同。
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 的脑回路不太一样。