普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月19日首页

用了这么久React,你真的搞懂useEffect了吗?

2025年9月19日 07:34

你是不是也遇到过这样的场景?页面刚加载时数据一片空白,需要手动刷新才能显示;组件里设置了定时器,结果切换页面后还在后台疯狂运行;甚至有时候,明明代码写得没问题,却出现了奇怪的内存泄漏问题……

别担心,这些问题我都遇到过!今天我就用一个超详细的指南,带你彻底搞懂React中的useEffect,让你从此告别这些烦人的坑。

读完本文,你将掌握useEffect的所有核心用法,包括数据获取、订阅机制、DOM操作,还能学会如何避免常见的内存泄漏问题。我会用大量代码示例和详细注释,让你一看就懂,一学就会!

什么是useEffect?为什么我们需要它?

简单来说,useEffect就是React函数组件中处理"副作用"的利器。什么是副作用?就是那些会影响组件外部世界的操作,比如数据获取、设置订阅、手动修改DOM等。

在类组件时代,我们通常在componentDidMount、componentDidUpdate和componentWillUnmount这些生命周期方法中处理这些操作。但现在有了函数组件和Hooks,useEffect就能帮我们统一处理所有这些场景。

让我们先看一个最简单的例子:

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

function Example() {
  const [count, setCount] = useState(0);

  // 类似于componentDidMount和componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `你点击了 ${count} 次`;
  });

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点我
      </button>
    </div>
  );
}

这段代码中,useEffect会在每次组件渲染后执行,更新文档的标题。这就是useEffect最基本的用法。

三种不同的依赖数组配置

useEffect最强大的地方在于它的第二个参数——依赖数组。通过不同的配置,我们可以控制effect的执行时机。

1. 每次渲染都执行

如果不传第二个参数,useEffect会在每次组件渲染后都执行:

useEffect(() => {
  console.log('这个effect在每次渲染后都会执行');
});

这种用法要谨慎,因为频繁执行可能会影响性能。

2. 只在首次渲染时执行

如果传递一个空数组[],effect只会在组件挂载时执行一次:

useEffect(() => {
  console.log('这个effect只在组件挂载时执行一次');
}, []); // 空依赖数组

这种模式非常适合数据获取操作,我们通常在这里发起API请求。

3. 在特定值变化时执行

如果数组中包含了某些值,effect会在这些值发生变化时执行:

useEffect(() => {
  console.log('这个effect在count变化时执行');
}, [count]); // count变化时重新执行

这种用法可以让我们在特定状态变化时执行相应的操作。

实际应用场景代码示例

场景一:数据获取

数据获取是useEffect最常见的用法之一。让我们看看如何正确地在组件中获取数据:

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 定义一个异步函数来获取数据
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('网络请求失败');
        }
        const userData = await response.json();
        setUser(userData);
        setError(null);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [userId]); // 当userId变化时重新获取数据

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

注意这里我们将userId作为依赖项,这样当userId变化时,组件会自动重新获取对应用户的数据。

场景二:订阅和取消订阅

在处理实时数据或事件监听时,我们需要正确设置订阅和取消订阅:

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

function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    // 模拟一个订阅函数
    function handleStatusChange(status) {
      setIsOnline(status);
    }

    // 创建订阅
    console.log('创建订阅');
    // 这里通常是建立WebSocket连接或事件监听
    const subscription = {
      unsubscribe: () => console.log('取消订阅')
    };

    // 设置初始状态
    handleStatusChange(true);

    // 返回清理函数,在组件卸载时执行
    return () => {
      console.log('执行清理');
      subscription.unsubscribe();
    };
  }, []); // 空数组表示只在挂载和卸载时执行

  if (isOnline === null) {
    return <div>加载中...</div>;
  }

  return <div>用户{isOnline ? '在线' : '离线'}</div>;
}

关键点是我们在effect中返回了一个清理函数,这个函数会在组件卸载时自动调用,确保我们不会留下任何内存泄漏。

场景三:手动操作DOM

虽然React推荐使用声明式的方式操作UI,但有时候我们确实需要直接操作DOM:

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

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦到输入框
    if (inputRef.current) {
      inputRef.current.focus();
      console.log('输入框已获得焦点');
    }
  }, []); // 空依赖数组,只在挂载时执行

  return <input ref={inputRef} type="text" placeholder="自动获得焦点的输入框" />;
}

这个例子展示了如何使用useRef和useEffect配合来实现自动聚焦功能。

常见坑点及如何避免

坑点一:忘记清理工作

这是最常见的useEffect错误之一。如果我们设置了订阅、定时器或事件监听,但忘记清理,就会导致内存泄漏:

// ❌ 错误示例:设置了定时器但没有清理
useEffect(() => {
  const interval = setInterval(() => {
    console.log('定时器运行中...');
  }, 1000);
  // 忘记返回清理函数!
}, []);

// ✅ 正确示例:返回清理函数
useEffect(() => {
  const interval = setInterval(() => {
    console.log('定时器运行中...');
  }, 1000);
  
  // 返回清理函数
  return () => {
    clearInterval(interval);
    console.log('定时器已清理');
  };
}, []);

坑点二:错误的依赖数组

依赖数组配置错误会导致各种奇怪的问题:

function Counter() {
  const [count, setCount] = useState(0);
  
  // ❌ 错误:缺少count依赖
  useEffect(() => {
    console.log(`Count: ${count}`);
  }, []); // 缺少count依赖,effect不会在count变化时重新执行
  
  // ✅ 正确:包含所有依赖
  useEffect(() => {
    console.log(`Count: ${count}`);
  }, [count]); // 正确包含count依赖
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

如果你使用的是ESLint,强烈建议启用react-hooks/exhaustive-deps规则,它会自动检测缺失的依赖项。

坑点三:无限循环

不正确的依赖项配置可能导致无限重新渲染:

// ❌ 错误示例:导致无限循环
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(count + 1); // 每次effect执行都更新状态,导致重新渲染
}); // 没有依赖数组,每次渲染后都执行

// ✅ 正确示例:使用函数式更新避免无限循环
useEffect(() => {
  setCount(prevCount => prevCount + 1);
}, []); // 只在挂载时执行一次

高级技巧和最佳实践

1. 使用多个useEffect分离关注点

与类组件中所有生命周期逻辑混在一起不同,我们可以使用多个useEffect来分离不同的逻辑:

function FriendStatus({ friendId }) {
  const [status, setStatus] = useState(null);
  const [profile, setProfile] = useState(null);

  // 处理状态订阅
  useEffect(() => {
    const subscription = subscribeToStatus(friendId, setStatus);
    return () => subscription.unsubscribe();
  }, [friendId]);

  // 处理资料获取
  useEffect(() => {
    let ignore = false;
    
    async function fetchProfile() {
      const profileData = await getProfile(friendId);
      if (!ignore) {
        setProfile(profileData);
      }
    }
    
    fetchProfile();
    
    return () => {
      ignore = true;
    };
  }, [friendId]);

  // 更多独立的effect...
}

这样让代码更加清晰,每个effect只负责一个特定的功能。

2. 使用自定义Hook抽象effect逻辑

如果某个effect逻辑在多个组件中都需要,我们可以将其提取为自定义Hook:

// 自定义Hook:useApi
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]); // 当url变化时重新获取

  return { data, loading, error };
}

// 在组件中使用自定义Hook
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

3. 使用useCallback和useMemo优化性能

当effect依赖于函数或对象时,为了避免不必要的重新执行,我们可以使用useCallback和useMemo:

function ProductList({ category, sortBy }) {
  const [products, setProducts] = useState([]);
  
  // 使用useCallback记忆化函数,避免每次渲染都创建新函数
  const fetchProducts = useCallback(async () => {
    const response = await fetch(`/api/products?category=${category}&sort=${sortBy}`);
    const data = await response.json();
    setProducts(data);
  }, [category, sortBy]); // 依赖项变化时重新创建函数
  
  useEffect(() => {
    fetchProducts();
  }, [fetchProducts]); // 现在effect依赖于记忆化的函数
  
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

还在硬邦邦跳转页面?Vue这3招让应用丝滑如德芙!

2025年9月19日 07:33

你是不是也遇到过这种情况?页面切换生硬得像老式电视机换台,数据加载时用户一脸懵逼不知道发生了什么,列表操作毫无反馈让人怀疑到底点没点上...

别急!今天我就带你用Vue的过渡动画三招,让你的应用瞬间从"机械僵硬"变身"丝滑流畅",用户体验直接提升一个level!

第一招:基础CSS过渡,简单又高效

先来看看最基础的CSS过渡效果。Vue提供了<transition>组件,包裹一下就能让元素动起来!

<template>
  <div>
    <button @click="show = !show">切换显示</button>
    
    <transition name="fade">
      <p v-if="show">你好呀!我会淡入淡出哦~</p>
    </transition>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: true
    }
  }
}
</script>

<style>
/* 定义进入和离开时的动画 */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.5s ease;
}

/* 定义进入开始和离开结束时的状态 */
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

这里有个小秘密:Vue会自动帮我们在不同阶段添加不同的class:

  • fade-enter:进入动画开始前(第一帧)
  • fade-enter-active:进入动画过程中
  • fade-enter-to:进入动画结束后
  • fade-leave:离开动画开始前
  • fade-leave-active:离开动画过程中
  • fade-leave-to:离开动画结束后

第二招:CSS动画,让效果更丰富

如果觉得简单的过渡不够酷,试试CSS动画吧!用法和过渡差不多,但能做出更复杂的效果。

<template>
  <div>
    <button @click="show = !show">蹦出来!</button>
    
    <transition name="bounce">
      <p v-if="show" class="animated-text">看我弹跳登场!</p>
    </transition>
  </div>
</template>

<style>
/* 弹跳动画 */
.bounce-enter-active {
  animation: bounce-in 0.5s;
}

.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

.animated-text {
  background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
  padding: 10px;
  border-radius: 5px;
  color: white;
}
</style>

这个效果特别适合重要提示或者操作反馈,让用户一眼就能注意到!

第三招:列表过渡,让数据动起来

实际项目中我们经常要处理列表数据,<transition-group>就是专门为列表设计的动画组件。

<template>
  <div>
    <button @click="addItem">添加项目</button>
    <button @click="removeItem">删除项目</button>
    
    <transition-group name="list" tag="ul">
      <li v-for="item in items" :key="item.id" class="list-item">
        {{ item.text }}
      </li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, text: '第一项' },
        { id: 2, text: '第二项' },
        { id: 3, text: '第三项' }
      ],
      nextId: 4
    }
  },
  methods: {
    addItem() {
      this.items.push({
        id: this.nextId++,
        text: `新项目 ${this.nextId}`
      })
    },
    removeItem() {
      this.items.pop()
    }
  }
}
</script>

<style>
.list-item {
  transition: all 0.5s;
  margin: 5px 0;
  padding: 10px;
  background: #f8f9fa;
  border-left: 4px solid #4ecdc4;
}

.list-enter-active, .list-leave-active {
  transition: all 0.5s;
}

.list-enter, .list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 让删除的项目先收缩再消失 */
.list-leave-active {
  position: absolute;
}
</style>

注意这里有两个重点:一是必须给每个列表项设置唯一的key,二是<transition-group>默认渲染为span,可以用tag属性指定为其他标签。

进阶玩法:JavaScript钩子函数

有时候CSS动画满足不了复杂需求,这时候就需要JavaScript钩子出场了!

<template>
  <div>
    <button @click="show = !show">切换</button>
    
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @after-enter="afterEnter"
      @enter-cancelled="enterCancelled"
      @before-leave="beforeLeave"
      @leave="leave"
      @after-leave="afterLeave"
      @leave-cancelled="leaveCancelled"
    >
      <div v-if="show" class="js-box">JS控制的动画</div>
    </transition>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: false
    }
  },
  methods: {
    // 进入动画开始前
    beforeEnter(el) {
      el.style.opacity = 0
      el.style.transform = 'scale(0)'
    },
    
    // 进入动画中
    enter(el, done) {
      // 使用requestAnimationFrame保证流畅性
      let start = null
      const duration = 600
      
      function animate(timestamp) {
        if (!start) start = timestamp
        const progress = timestamp - start
        
        // 计算当前进度(0-1)
        const percentage = Math.min(progress / duration, 1)
        
        // 应用动画效果
        el.style.opacity = percentage
        el.style.transform = `scale(${percentage})`
        
        if (progress < duration) {
          requestAnimationFrame(animate)
        } else {
          done() // 动画完成,调用done回调
        }
      }
      
      requestAnimationFrame(animate)
    },
    
    // 进入动画完成后
    afterEnter(el) {
      console.log('进入动画完成啦!')
    },
    
    // 进入动画被中断
    enterCancelled(el) {
      console.log('进入动画被取消了')
    },
    
    // 离开动画相关钩子...
    beforeLeave(el) {
      el.style.opacity = 1
      el.style.transform = 'scale(1)'
    },
    
    leave(el, done) {
      // 类似的实现离开动画...
      done()
    }
  }
}
</script>

<style>
.js-box {
  width: 100px;
  height: 100px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  margin-top: 10px;
}
</style>

JavaScript钩子虽然复杂一些,但是能实现任何你能想到的动画效果!

终极武器:集成第三方动画库

如果你想快速实现酷炫效果,又不想自己写太多CSS,那么第三方动画库就是你的最佳选择!

先安装Animate.css:

npm install animate.css

然后在项目中引入:

import 'animate.css'

使用起来超级简单:

<template>
  <div>
    <button @click="show = !show">来点炫酷的!</button>
    
    <transition
      enter-active-class="animate__animated animate__bounceIn"
      leave-active-class="animate__animated animate__bounceOut"
    >
      <div v-if="show" class="demo-box">哇!好酷!</div>
    </transition>
  </div>
</template>

<style>
.demo-box {
  width: 150px;
  height: 150px;
  background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  margin-top: 20px;
  font-size: 18px;
  font-weight: bold;
}
</style>

Animate.css提供了超多现成动画效果,比如:

  • animate__bounceIn(弹跳进入)
  • animate__fadeInUp(淡入上浮)
  • animate__flip(翻转效果)
  • animate__zoomIn(缩放进入)

想要什么效果,换个class名就行了,简直是懒人福音!

实战技巧:避免这些常见坑

用了这么久Vue动画,我也踩过不少坑,分享几个实用技巧:

  1. 动画闪烁问题:在初始渲染时避免使用v-if,可以用v-show或者通过CSS控制初始状态

  2. 列表动画优化:对于长列表,可以给<transition-group>设置tag="div",避免生成太多DOM节点

  3. 性能注意:尽量使用transform和opacity做动画,这两个属性不会触发重排,性能更好

  4. 移动端适配:在移动端注意动画时长,0.3s左右比较合适,不要太长

  5. 减少同时动画:同一时间不要有太多元素做动画,会影响性能

总结

好了,今天分享了Vue过渡动画的四大招式:从基础的CSS过渡,到更丰富的CSS动画,再到处理列表的<transition-group>,最后是强大的JavaScript钩子和第三方库集成。

记住,好的动画不是为了炫技,而是为了提升用户体验。适当的动画能让用户知道发生了什么,引导注意力,让操作更有反馈感。

❌
❌