MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 列表渲染中的动画效果

2022-08-122.1k 阅读

React 列表渲染基础回顾

在深入探讨 React 列表渲染中的动画效果之前,先来回顾一下 React 列表渲染的基本概念。在 React 中,使用 map 方法来渲染列表是一种常见的做法。例如,假设有一个包含多个待办事项的数组:

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  return (
    <ul>
      {todoItems.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

这里,map 方法遍历 todoItems 数组,并为每个元素返回一个 <li> 元素。key 属性是 React 用于高效更新列表的重要标识,它应该是列表中每个元素的唯一标识符。

基本的 CSS 过渡动画在列表中的应用

  1. 过渡效果基础
    • 可以通过 CSS 的 transition 属性为列表项添加简单的过渡动画。例如,为列表项的 opacity(透明度)添加过渡效果,当列表项进入或离开视图时,会有一个淡入淡出的效果。
    • 首先,创建一个 CSS 类,比如 fade - in - out
.fade - in - out {
  opacity: 1;
  transition: opacity 0.3s ease - in - out;
}
.fade - in - out.hidden {
  opacity: 0;
}
  • 然后在 React 组件中,根据列表项的状态来应用这个类。假设我们有一个删除列表项的功能,当点击删除按钮时,列表项应该有一个淡出的动画。
import React, { useState } from'react';

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  const [items, setItems] = useState(todoItems);
  const handleDelete = (id) => {
    setItems(items.filter(item => item.id!== id));
  };
  return (
    <ul>
      {items.map(todo => (
        <li key={todo.id} className={`fade - in - out ${!items.some(item => item.id === todo.id)? 'hidden' : ''}`}>
          {todo.text}
          <button onClick={() => handleDelete(todo.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

在这个例子中,当点击删除按钮时,handleDelete 函数会过滤掉要删除的列表项。同时,通过判断列表项是否还存在于 items 数组中,来决定是否添加 hidden 类,从而触发 opacity 的过渡动画。

  1. 过渡效果的局限性
    • 这种基本的 CSS 过渡动画虽然简单易用,但存在一些局限性。例如,当列表项数量较多且同时进行添加或删除操作时,所有列表项的动画会同时执行,缺乏层次感。而且,对于复杂的动画需求,如列表项的排序动画,单纯使用 CSS 过渡动画很难实现。

使用 React Transition Group 实现列表动画

  1. React Transition Group 简介

    • React Transition Group 是 React 的一个官方库,用于在 React 组件进入、离开或在挂载树中移动时添加动画。它提供了几个关键组件,如 TransitionCSSTransitionTransitionGroup
    • TransitionGroup 用于管理一组 TransitionCSSTransition 组件,它会自动为进入和离开的子组件添加或移除动画。
    • CSSTransition 则专门用于基于 CSS 的过渡和动画,它会根据组件的挂载和卸载状态自动添加和移除 CSS 类。
  2. 安装和基本使用

    • 首先,需要安装 react - transition - group 库:
npm install react - transition - group
  • 下面是一个使用 CSSTransitionTransitionGroup 实现列表项淡入淡出动画的示例:
import React, { useState } from'react';
import { CSSTransition, TransitionGroup } from'react - transition - group';

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  const [items, setItems] = useState(todoItems);
  const handleDelete = (id) => {
    setItems(items.filter(item => item.id!== id));
  };
  return (
    <TransitionGroup>
      {items.map(todo => (
        <CSSTransition
          key={todo.id}
          timeout={300}
          classNames="fade"
        >
          <li>
            {todo.text}
            <button onClick={() => handleDelete(todo.id)}>删除</button>
          </li>
        </CSSTransition>
      ))}
    </TransitionGroup>
  );
}

// 定义 CSS 类
const fadeStyles = {
  entering: { opacity: 0 },
  entered: { opacity: 1 },
  exiting: { opacity: 1 },
  exited: { opacity: 0 }
};

// 为 CSSTransition 生成对应的 CSS 类名
const fadeClassNames = {
  enter: 'fade - entering',
  enterActive: 'fade - entered',
  exit: 'fade - exiting',
  exitActive: 'fade - exited'
};

在这个示例中,TransitionGroup 包裹了所有的列表项,每个列表项都被 CSSTransition 组件包裹。CSSTransitionkey 属性确保每个列表项的动画是独立的。timeout 属性指定了动画的持续时间,classNames 属性指定了动画过程中使用的 CSS 类前缀。通过定义 fadeStylesfadeClassNames,可以为动画的不同阶段设置不同的 CSS 样式。

  1. 列表项排序动画
    • React Transition Group 还可以实现列表项的排序动画。假设我们有一个可以对列表项进行排序的功能,比如按照文本内容的字母顺序排序。
import React, { useState } from'react';
import { CSSTransition, TransitionGroup } from'react - transition - group';

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  const [items, setItems] = useState(todoItems);
  const handleSort = () => {
    setItems([...items].sort((a, b) => a.text.localeCompare(b.text)));
  };
  return (
    <div>
      <button onClick={handleSort}>排序</button>
      <TransitionGroup>
        {items.map(todo => (
          <CSSTransition
            key={todo.id}
            timeout={300}
            classNames="sort - fade"
          >
            <li>
              {todo.text}
            </li>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
}

// 定义排序动画的 CSS 类
const sortFadeStyles = {
  entering: { opacity: 0, transform: 'translateY(-10px)' },
  entered: { opacity: 1, transform: 'translateY(0)' },
  exiting: { opacity: 1, transform: 'translateY(0)' },
  exited: { opacity: 0, transform: 'translateY(10px)' }
};

const sortFadeClassNames = {
  enter:'sort - fade - entering',
  enterActive:'sort - fade - entered',
  exit:'sort - fade - exiting',
  exitActive:'sort - fade - exited'
};

在这个例子中,点击“排序”按钮会触发 handleSort 函数,该函数会对列表项进行排序。CSSTransition 组件为每个列表项在排序过程中添加了淡入淡出和位移的动画效果,使得列表项的排序过程更加平滑和直观。

使用 GSAP 实现复杂列表动画

  1. GSAP 简介

    • GSAP(GreenSock Animation Platform)是一个强大的 JavaScript 动画库,它提供了丰富的动画功能,包括时间轴控制、缓动函数等。在 React 项目中使用 GSAP 可以实现比 CSS 过渡动画和 React Transition Group 更复杂的列表动画效果。
    • GSAP 有几个核心的方法,如 TweenMax.to() 用于创建单个动画,TimelineMax 用于创建动画时间轴,将多个动画组合在一起。
  2. 安装和基本使用

    • 首先,安装 GSAP:
npm install gsap
  • 以下是一个使用 GSAP 为列表项添加逐个淡入动画的示例:
import React, { useEffect } from'react';
import { TweenMax } from 'gsap';

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  useEffect(() => {
    const listItems = document.querySelectorAll('li');
    listItems.forEach((item, index) => {
      TweenMax.to(item, 0.5, {
        opacity: 1,
        y: 0,
        delay: index * 0.1,
        ease: 'Power2.easeInOut'
      });
    });
  }, []);
  return (
    <ul style={{ opacity: 0 }}>
      {todoItems.map(todo => (
        <li key={todo.id} style={{ opacity: 0, y: 10 }}>{todo.text}</li>
      ))}
    </ul>
  );
}

在这个示例中,useEffect 钩子在组件挂载后执行。通过 document.querySelectorAll('li') 获取所有的列表项,然后使用 TweenMax.to() 方法为每个列表项创建一个淡入和向下位移的动画。delay 属性使得每个列表项的动画有一定的延迟,从而实现逐个淡入的效果。ease 属性指定了缓动函数,这里使用的是 Power2.easeInOut,使得动画有一个平滑的加速和减速过程。

  1. GSAP 时间轴在列表动画中的应用
    • 假设我们想要实现一个更复杂的列表动画,比如列表项在删除时,不仅有淡出动画,还会有一个收缩的动画,并且其他列表项会有一个向上移动填补空缺的动画。可以使用 GSAP 的 TimelineMax 来实现。
import React, { useState, useEffect } from'react';
import { TimelineMax, TweenMax } from 'gsap';

const todoItems = [
  { id: 1, text: '学习 React' },
  { id: 2, text: '完成项目' },
  { id: 3, text: '阅读文档' }
];

function TodoList() {
  const [items, setItems] = useState(todoItems);
  const handleDelete = (id) => {
    const itemIndex = items.findIndex(item => item.id === id);
    const newItems = items.filter(item => item.id!== id);
    const timeline = new TimelineMax();
    const listItems = document.querySelectorAll('li');
    timeline.to(listItems[itemIndex], 0.3, {
      opacity: 0,
      scale: 0.5,
      onComplete: () => {
        setItems(newItems);
      }
    });
    for (let i = itemIndex; i < newItems.length; i++) {
      timeline.to(listItems[i + 1], 0.3, {
        y: '-=30px',
        delay: 0.1 * i
      });
    }
    timeline.play();
  };
  useEffect(() => {
    const listItems = document.querySelectorAll('li');
    listItems.forEach((item, index) => {
      TweenMax.to(item, 0.5, {
        opacity: 1,
        y: 0,
        delay: index * 0.1,
        ease: 'Power2.easeInOut'
      });
    });
  }, []);
  return (
    <ul>
      {items.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => handleDelete(todo.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

在这个示例中,handleDelete 函数在点击删除按钮时被调用。首先找到要删除的列表项的索引,然后创建一个 TimelineMax 实例。在时间轴中,先为要删除的列表项添加淡出和收缩的动画,并在动画完成后更新列表数据。接着,为后续的列表项添加向上移动的动画,每个动画之间有一定的延迟,从而实现一个连贯的删除和填补空缺的动画效果。

性能优化与注意事项

  1. 性能优化
    • 批量更新:在 React 中,频繁的状态更新会导致不必要的重新渲染。当涉及到列表动画时,尤其是对列表项进行批量操作(如同时添加或删除多个列表项),可以使用 ReactDOM.unstable_batchedUpdates(在 React 18 之前)或 flushSync(在 React 18 及之后)来批量处理状态更新,从而减少不必要的渲染。例如:
import React, { useState } from'react';
import { flushSync } from'react - dom';

function List() {
  const [items, setItems] = useState([]);
  const addMultipleItems = () => {
    flushSync(() => {
      for (let i = 0; i < 10; i++) {
        setItems([...items, { id: i, text: `Item ${i}` }]);
      }
    });
  };
  return (
    <div>
      <button onClick={addMultipleItems}>添加多个项</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}
  • 避免过度动画:虽然动画可以提升用户体验,但过多或复杂的动画可能会导致性能问题,尤其是在移动设备上。尽量保持动画简洁,避免使用过于复杂的动画效果,如大量的 3D 变换或高帧率的动画。
  • 使用 requestAnimationFrame:对于一些需要精确控制动画帧的场景,可以使用 requestAnimationFrame。GSAP 等库内部也会利用 requestAnimationFrame 来优化动画性能。例如,在一个自定义的列表项滚动动画中,可以使用 requestAnimationFrame 来确保动画在浏览器的下一帧绘制之前执行,从而实现平滑的动画效果。
  1. 注意事项
    • key 属性的重要性:在列表渲染中,key 属性始终是至关重要的。不正确的 key 可能会导致动画异常,比如列表项的动画效果错乱,或者动画无法正常触发。确保 key 是列表项的唯一标识符,并且在列表项的状态发生变化时,key 不会改变。
    • 兼容性:不同的动画库和 CSS 动画属性在不同的浏览器中有不同的兼容性。在使用动画效果时,要注意检查浏览器兼容性,并考虑使用 Polyfill 来确保在旧版本浏览器中也能正常显示动画。例如,一些 CSS 3D 变换属性在某些老版本的浏览器中可能不支持,这时可以使用 transform - polyfill 等工具来提供兼容性。
    • 动画与无障碍性:在添加动画效果时,要考虑无障碍性。例如,一些动画可能会对患有眩晕症或其他视觉障碍的用户造成不适。可以通过提供关闭动画的选项,或者使用 prefers - reduced - motion 媒体查询来检测用户是否偏好减少运动,从而为这些用户提供更友好的体验。例如:
@media (prefers - reduced - motion: reduce) {
 .fade - in - out {
    transition: none;
  }
}

这样,当用户设备设置为偏好减少运动时,相关的动画会被禁用,提升了无障碍性。

不同场景下的动画选择策略

  1. 简单过渡场景

    • 当只需要简单的淡入淡出、颜色变化或基本的位移等过渡效果时,使用 CSS 过渡动画是一个不错的选择。它简单直观,不需要引入额外的库,并且性能开销相对较小。例如,在一个简单的待办事项列表中,当列表项被标记为已完成时,使用 CSS 过渡动画为其添加一个淡入淡出的效果,以表示状态的改变。
    • 可以通过在 React 组件中动态添加和移除 CSS 类来控制动画的触发,如前面提到的基本 CSS 过渡动画示例。这种方式适合于对动画复杂度要求不高,并且希望快速实现基本动画效果的场景。
  2. 复杂交互场景

    • 对于涉及到复杂交互的列表动画,如列表项的排序、拖拽、分组等操作,React Transition Group 或 GSAP 更为合适。React Transition Group 提供了基于 React 组件生命周期的动画管理,适合于处理列表项的进入、离开和移动等基本动画场景。而 GSAP 则具有更强大的动画控制能力,如时间轴、缓动函数等,能够实现更加复杂的动画效果。
    • 例如,在一个可拖拽排序的任务列表中,使用 React Transition Group 可以方便地为列表项的排序过程添加过渡动画,使其移动更加平滑。而如果需要在拖拽过程中添加更复杂的动画效果,如列表项的缩放、旋转等,GSAP 则能更好地满足需求。
  3. 性能敏感场景

    • 在性能敏感的场景下,如移动应用或处理大量列表项的场景,需要谨慎选择动画方案。尽量减少动画的复杂度,优先使用简单的 CSS 过渡动画或利用浏览器的硬件加速功能的动画。例如,使用 transform 属性的动画通常比改变 widthheight 等属性的动画性能更好,因为 transform 动画可以利用浏览器的 GPU 加速。
    • 同时,在使用动画库时,要注意库的性能开销。一些功能强大的动画库可能会引入较大的代码体积和性能负担,在这种情况下,需要权衡功能和性能,选择最适合的动画方案。

与 React 生态其他技术结合实现动画

  1. 与 Redux 结合
    • 在使用 Redux 管理状态的 React 应用中,可以通过 Redux 的 action 和 reducer 来触发列表动画。例如,当一个列表项的状态在 Redux 中发生变化(如从“未完成”变为“已完成”)时,可以通过 dispatch 一个 action 来触发相应的动画。
    • 首先,在 Redux 的 action 中定义动画相关的 action type,比如 ITEM_COMPLETED_ANIMATION
// actions.js
const ITEM_COMPLETED_ANIMATION = 'ITEM_COMPLETED_ANIMATION';

export const completeItemAnimation = (id) => ({
  type: ITEM_COMPLETED_ANIMATION,
  payload: { id }
});
  • 然后在 reducer 中处理这个 action:
// reducer.js
const initialState = {
  items: []
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ITEM_COMPLETED_ANIMATION:
      // 这里可以对状态进行更新,同时触发动画相关的逻辑
      return {
      ...state
      };
    default:
      return state;
  }
};

export default todoReducer;
  • 在 React 组件中,通过 connectuseSelectoruseDispatch 等 hooks 来连接 Redux 状态和 dispatch action:
import React from'react';
import { useDispatch } from'react - redux';
import { completeItemAnimation } from './actions';

function TodoItem({ item }) {
  const dispatch = useDispatch();
  const handleComplete = () => {
    dispatch(completeItemAnimation(item.id));
  };
  return (
    <li>
      {item.text}
      <button onClick={handleComplete}>完成</button>
    </li>
  );
}

export default TodoItem;
  • 这里通过 Redux 来管理列表项状态变化,进而触发动画。可以结合 React Transition Group 或 GSAP 等动画库,在 action 触发后为列表项添加相应的动画效果。
  1. 与 React Router 结合
    • 在单页应用中,使用 React Router 进行页面导航。可以在页面切换时,为列表元素添加动画效果。例如,当从一个包含列表的页面导航到另一个页面时,列表元素可以有一个淡出的动画。
    • 首先,在路由配置中,可以使用 TransitionGroupCSSTransition 来包裹路由组件:
import React from'react';
import { BrowserRouter as Router, Routes, Route, NavLink } from'react - router - dom';
import { CSSTransition, TransitionGroup } from'react - transition - group';
import Home from './Home';
import About from './About';

function App() {
  return (
    <Router>
      <div>
        <nav>
          <NavLink to="/">首页</NavLink>
          <NavLink to="/about">关于</NavLink>
        </nav>
        <TransitionGroup>
          <Routes>
            <Route path="/" element={
              <CSSTransition
                key="home"
                timeout={300}
                classNames="page - fade"
              >
                <Home />
              </CSSTransition>
            } />
            <Route path="/about" element={
              <CSSTransition
                key="about"
                timeout={300}
                classNames="page - fade"
              >
                <About />
              </CSSTransition>
            } />
          </Routes>
        </TransitionGroup>
      </div>
    </Router>
  );
}

export default App;
  • 然后定义 page - fade 的 CSS 类来实现页面切换时的淡入淡出动画:
.page - fade - entering {
  opacity: 0;
}
.page - fade - entered {
  opacity: 1;
}
.page - fade - exiting {
  opacity: 1;
}
.page - fade - exited {
  opacity: 0;
}
  • 如果页面中的列表元素也需要特殊的动画效果,可以在页面组件(如 Home 组件)中,结合前面提到的列表动画方法,在页面切换时为列表元素添加相应的动画,使整个页面导航过程更加流畅和美观。

通过以上对 React 列表渲染中动画效果的深入探讨,从基本的 CSS 过渡动画到使用强大的动画库,再到与 React 生态其他技术的结合,希望能帮助开发者在 React 项目中实现丰富多样且性能良好的列表动画,提升用户体验。