useEffect 为什么在组件首次渲染时就执行了?

码农翌耀 阅读 24

我刚学 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>
我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
一锦灏
一锦灏 Lv1
useEffect 在组件首次渲染时执行其实是有其设计初衷的。React 的 useEffect 和 Vue 的 watch 在行为上有一些差异,主要是因为它们的设计哲学不同。下面我会详细解释一下。

首先,useEffect 的第一个参数是一个函数,这个函数会在依赖项数组发生变化时执行。如果你传递一个空数组作为第二个参数,那么 useEffect 就会在组件挂载时执行一次,并且之后不再执行,除非组件卸载再重新挂载。

这种行为背后的原因是为了确保副作用(side effects)能够在组件挂载后立即执行,这对于很多场景来说是非常必要的。比如数据获取、订阅事件等操作,我们希望这些操作在组件显示出来后立刻进行。

在你的例子中,Vue 的 watch 默认不会在初始绑定时触发回调,只有当被监视的数据发生变化时才会执行。这是因为 Vue 的 watch 更倾向于只关注数据的变化,而不是组件的生命周期。

如果你希望 useEffect 在首次渲染时不执行,可以考虑使用一个布尔值标志来控制:

import React, { useEffect, useState } from 'react';

function MyComponent() {
const [count, setCount] = useState(0);
const [isInitialMount, setIsInitialMount] = useState(true);

useEffect(() => {
if (isInitialMount) {
setIsInitialMount(false);
return; // 首次渲染时直接返回,不执行副作用
}

console.log('count changed:', count);
}, [count, isInitialMount]);

return (

Count: {count}




);
}

export default MyComponent;


在这个例子中,我们引入了一个 isInitialMount 的状态来跟踪组件是否是首次渲染。如果是首次渲染,我们就直接返回,不执行副作用。这样就能达到类似于 Vue 的 watch 的效果。

需要注意的是,这种方式可能会让你的代码稍微复杂一些,所以在使用之前要权衡一下利弊。有时候,让 useEffect 在首次渲染时执行也是合理的,取决于你的具体需求。
点赞
2026-03-20 20:08
IT人克培
这完全正常,你遇到的是 React 和 Vue 在设计哲学上的根本差异。我以前刚从 Vue 转 React 时也在这儿绕过弯子,咱们把这事儿掰扯清楚。

先说结论,useEffect 的设计初衷就是"渲染后执行副作用",它的语义是"当组件渲染完成之后,我要做点什么",而不是"当数据变化时我要做点什么"。这两者看起来很像,但出发点完全不同。

React 的 useEffect 执行逻辑是这样的:组件第一次渲染完成后执行一次,之后每次依赖项变化导致重新渲染后,也会执行。你传了空数组 [] 作为依赖,意思是"我不依赖任何 props 或 state",所以后续渲染不会再触发这个 effect,但第一次挂载后的那次执行是雷打不动的。

Vue 的 watch 默认是惰性的,它只关注"变化"这个动作,没有变化就不触发。而 useEffect 关注的是"渲染完成"这个时间点,渲染完成了就要执行。

来看一段代码对比两者的差异:

// React useEffect
useEffect(() => {
console.log('我执行了');
}, []); // 空依赖数组 = 只在挂载后执行一次,之后不再执行

// Vue watch(默认行为)
watch(count, (newVal) => {
console.log('count changed:', newVal);
}); // 初始不执行,只有 count 变化时才执行

// Vue watch(immediate 模式)
watch(count, (newVal) => {
console.log('count changed:', newVal);
}, { immediate: true }); // 这就类似 useEffect 了,会立即执行一次


你会发现 Vue 也有 immediate: true 选项,开启后行为就跟 React 的 useEffect 一样了。这说明两边都意识到了"立即执行"这个场景的必要性,只是默认值选得不一样。

为什么 React 要这样设计?因为 React 团队认为,很多副作用的逻辑结构是这样的:执行某些操作,然后在组件卸载或下次执行前清理。比如订阅事件、开启定时器、发起网络请求。这些操作在首次渲染后就需要执行,而不是等到数据变化。

一个典型的例子:

useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);

return () => clearInterval(timer); // 清理函数
}, []);


如果 useEffect 不在首次渲染后执行,这个定时器就不会启动,你还得另外找个地方去初始化它,逻辑就散了。

那如果你真的想要"只在数据变化时执行,首次不触发"的行为呢?可以用 useRef 配合一个标志位来实现:

const isFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return; // 首次渲染直接返回,不执行后续逻辑
}

console.log('count 真的变化了:', count);
}, [count]);


或者封装成一个自定义 hook,网上有现成的库比如 useUpdateEffect,专门干这事儿。

需要注意一点,不要把 useEffect 理解成 Vue 的 watch。useEffect 是声明式的,你告诉 React"渲染后我要做这些事",React 会保证在正确的时机调用。而 watch 是响应式的,你告诉 Vue"这个数据变了你要做什么"。思维模式不一样,用 useEffect 去模拟 watch 的行为,写出来的代码往往很别扭。

总结一下:useEffect 首次执行是设计如此,空数组只是控制"后续是否因依赖变化而再次执行",控制不了首次。习惯了就好,React 这套设计用多了你会发现它在处理副作用时其实挺优雅的,只是跟 Vue 的脑回路不太一样。
点赞 1
2026-03-02 22:20