ReactUI 渲染与交互

第五章 UI 渲染与交互

React 的核心是构建用户界面,本章将深入探讨 React 的渲染机制、优化策略以及交互处理的最佳实践,帮助你构建高性能、响应式的用户界面。

5.1 React 渲染原理

5.1.1 Virtual DOM 与 Diff 算法

Virtual DOM 工作原理:

// 1. Virtual DOM 概念演示
const VirtualDOMExample = () => {
  // React Element 对象(Virtual DOM)
  const virtualElement = React.createElement(
    'div',
    { className: 'container', id: 'main' },
    React.createElement('h1', null, 'Hello Virtual DOM'),
    React.createElement('p', null, 'This is a virtual representation')
  );

  // 等价的 JSX
  const jsxElement = (
    <div className="container" id="main">
      <h1>Hello Virtual DOM</h1>
      <p>This is a virtual representation</p>
    </div>
  );

  // 2. Diff 算法演示
  const [count, setCount] = React.useState(0);
  const [items, setItems] = React.useState(['Apple', 'Banana']);

  // Diff 算法如何工作:
  // - 类型不同:销毁旧节点,创建新节点
  // - 类型相同:比较 props,更新变化
  // - 列表子元素:通过 key 进行标识和重排

  const addItem = () => {
    // ✅ 好的做法:使用 key 进行高效 diff
    setItems(prev => [...prev, `Item ${prev.length + 1}`]);
  };

  const shuffleItems = () => {
    // ✅ 好的做法:保持 key 稳定
    setItems(prev => {
      const shuffled = [...prev].sort(() => Math.random() - 0.5);
      return shuffled;
    });
  };

  // ❌ 不好的做法:没有使用 key
  const BadList = ({ items }) => (
    <ul>
      {items.map(item => (
        <li>{item}</li> // 缺少 key,React 无法有效识别元素
      ))}
    </ul>
  );

  // ✅ 好的做法:使用稳定的 key
  const GoodList = ({ items }) => (
    <ul>
      {items.map((item, index) => (
        <li key={item.id || item}>{item}</li> // 使用稳定的唯一标识
      ))}
    </ul>
  );

  return (
    <div className="virtual-dom-demo">
      <h2>Virtual DOM & Diff Algorithm Demo</h2>
      
      <div className="counter-demo">
        <p>Count: {count}</p>
        <button onClick={() => setCount(prev => prev + 1)}>
          Increment
        </button>
        <button onClick={() => setCount(prev => prev + 1)}>
          Increment
        </button>
        <button onClick={() => setCount(prev => prev + 1)}>
          Increment
        </button>
        <p>
          React 会批量更新这三个 setState 调用,
          只触发一次重新渲染。
        </p>
      </div>

      <div className="list-demo">
        <h3>List Rendering with Keys</h3>
        <button onClick={addItem}>Add Item</button>
        <button onClick={shuffleItems}>Shuffle Items</button>
        
        <GoodList items={items} />
        
        <div className="key-explanation">
          <h4>Key Importance:</h4>
          <ul>
            <li>Key 帮助 React 识别哪些元素发生了变化</li>
            <li>稳定的 key 确保 DOM 节点被重用而非重建</li>
            <li>避免使用数组索引作为 key(除非列表是静态的)</li>
          </ul>
        </div>
      </div>
    </div>
  );
};

// 3. 渲染流程演示
const RenderingFlowDemo = () => {
  const [user, setUser] = React.useState(null);
  const [posts, setPosts] = React.useState([]);
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    console.log('1. Component rendered or updated');
  });

  React.useEffect(() => {
    console.log('2. User state changed:', user);
  }, [user]);

  React.useEffect(() => {
    console.log('3. Posts state changed:', posts);
  }, [posts]);

  const fetchUserData = async () => {
    setLoading(true);
    console.log('4. Fetch started');
    
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    // 批量更新状态
    React.startTransition(() => {
      setUser({ id: 1, name: 'John Doe' });
      setPosts([{ id: 1, title: 'Hello React' }]);
      setLoading(false);
    });
    
    console.log('5. Batch state updates triggered');
  };

  const renderCount = React.useRef(0);
  renderCount.current++;
  console.log(`Component rendered ${renderCount.current} times`);

  return (
    <div className="rendering-flow-demo">
      <h2>Rendering Flow Demo</h2>
      
      <button onClick={fetchUserData} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch User Data'}
      </button>

      {loading && <p>Loading user data...</p>}
      
      {user && (
        <div className="user-info">
          <h3>User: {user.name}</h3>
        </div>
      )}
      
      {posts.length > 0 && (
        <div className="posts">
          <h3>Posts:</h3>
          {posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      )}

      <div className="render-info">
        <p>Render Count: {renderCount.current}</p>
        <p>Check console for rendering flow logs</p>
      </div>
    </div>
  );
};

5.1.2 渲染性能优化

组件渲染优化:

import React, { memo, useMemo, useCallback, useState, useRef } from 'react';

// 1. React.memo 防止不必要的重新渲染
const ExpensiveComponent = memo(({ 
  data, 
  onAction, 
  style 
}) => {
  // 只有当 props 发生实际变化时才重新渲染
  console.log('ExpensiveComponent rendered');
  
  const expensiveCalculation = useMemo(() => {
    console.log('Performing expensive calculation...');
    // 模拟昂贵计算
    return data.reduce((sum, item) => sum + item.value * item.multiplier, 0);
  }, [data]);

  return (
    <div style={style} className="expensive-component">
      <h3>Expensive Calculation Result: {expensiveCalculation}</h3>
      <button onClick={() => onAction('calculate')}>
        Recalculate
      </button>
    </div>
  );
}, (prevProps, nextProps) => {
  // 自定义比较函数
  const propsEqual = 
    JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) &&
    prevProps.style === nextProps.style;
  
  console.log('Props comparison result:', propsEqual);
  return propsEqual;
});

// 2. useMemo 缓存计算结果
const CalculationDemo = () => {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
  const [multiplier, setMultiplier] = useState(2);
  const [unrelatedState, setUnrelatedState] = useState(0);

  // ❌ 不好的做法:每次渲染都重新计算
  // const sum = numbers.reduce((acc, num) => acc + num * multiplier, 0);

  // ✅ 好的做法:使用 useMemo 缓存计算结果
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return numbers.reduce((acc, num) => acc + num * multiplier, 0);
  }, [numbers, multiplier]);

  const sortedNumbers = useMemo(() => {
    console.log('Sorting numbers...');
    return [...numbers].sort((a, b) => a - b);
  }, [numbers]);

  const expensiveData = useMemo(() => {
    console.log('Generating expensive data...');
    return Array.from({ length: 1000 }, (_, i) => ({
      id: i,
      value: Math.random() * 100,
      processed: Math.sin(i) * Math.cos(i)
    }));
  }, []); // 空依赖数组,只在首次渲染时计算

  return (
    <div className="calculation-demo">
      <h3>Calculation Demo</h3>
      
      <div className="controls">
        <button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
          Add Number
        </button>
        <button onClick={() => setMultiplier(multiplier + 1)}>
          Multiplier: {multiplier}
        </button>
        <button onClick={() => setUnrelatedState(unrelatedState + 1)}>
          Unrelated State: {unrelatedState}
        </button>
      </div>

      <div className="results">
        <p>Numbers: {numbers.join(', ')}</p>
        <p>Sum: {sum}</p>
        <p>Sorted: {sortedNumbers.join(', ')}</p>
        <p>Expensive data length: {expensiveData.length}</p>
      </div>

      <ExpensiveComponent 
        data={expensiveData.slice(0, 10)} 
        onAction={(action) => console.log('Action:', action)}
        style={{ backgroundColor: '#f0f0f0' }}
      />
    </div>
  );
};

// 3. useCallback 缓存函数引用
const CallbackDemo = () => {
  const [count, setCount] = useState(0);
  const [list, setList] = useState(['Apple', 'Banana', 'Orange']);

  // ❌ 不好的做法:每次渲染都创建新函数
  // const addItem = () => setList(prev => [...prev, `Item ${prev.length + 1}`]);
  // const increment = () => setCount(prev => prev + 1);

  // ✅ 好的做法:使用 useCallback 缓存函数
  const addItem = useCallback(() => {
    setList(prev => [...prev, `Item ${prev.length + 1}`]);
  }, []); // 空依赖数组,函数引用稳定

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // 空依赖数组,函数引用稳定

  const removeItem = useCallback((index) => {
    setList(prev => prev.filter((_, i) => i !== index));
  }, []); // 空依赖数组,函数引用稳定

  const clearList = useCallback(() => {
    setList([]);
  }, []); // 空依赖数组,函数引用稳定

  return (
    <div className="callback-demo">
      <h3>Callback Demo</h3>
      
      <div className="counter">
        <p>Count: {count}</p>
        <button onClick={increment}>Increment</button>
      </div>

      <div className="list-manager">
        <p>List Items: {list.length}</p>
        <button onClick={addItem}>Add Item</button>
        <button onClick={clearList}>Clear List</button>
        
        <ChildList 
          items={list}
          onRemove={removeItem}
        />
      </div>
    </div>
  );
};

// 子组件 - 使用 memo 优化
const ChildList = memo(({ items, onRemove }) => {
  console.log('ChildList rendered');
  
  return (
    <ul className="child-list">
      {items.map((item, index) => (
        <li key={`${item}-${index}`}>
          {item}
          <button onClick={() => onRemove(index)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
});

// 4. 状态提升优化
const StateLiftingDemo = () => {
  const [sharedState, setSharedState] = useState({
    filter: 'all',
    sortBy: 'name',
    viewMode: 'grid'
  });

  const [data, setData] = useState([
    { id: 1, name: 'Item 1', category: 'A' },
    { id: 2, name: 'Item 2', category: 'B' },
    { id: 3, name: 'Item 3', category: 'A' },
    { id: 4, name: 'Item 4', category: 'C' }
  ]);

  // 将共享状态管理提升到父组件
  const updateSharedState = useCallback((updates) => {
    setSharedState(prev => ({ ...prev, ...updates }));
  }, []);

  // 计算派生状态
  const filteredAndSortedData = useMemo(() => {
    let result = [...data];
    
    // 过滤
    if (sharedState.filter !== 'all') {
      result = result.filter(item => item.category === sharedState.filter);
    }
    
    // 排序
    result.sort((a, b) => {
      switch (sharedState.sortBy) {
        case 'name': return a.name.localeCompare(b.name);
        case 'category': return a.category.localeCompare(b.category);
        default: return 0;
      }
    });
    
    return result;
  }, [data, sharedState.filter, sharedState.sortBy]);

  return (
    <div className="state-lifting-demo">
      <h3>State Lifting Demo</h3>
      
      <ControlPanel 
        sharedState={sharedState}
        onUpdate={updateSharedState}
      />
      
      <DataDisplay 
        data={filteredAndSortedData}
        viewMode={sharedState.viewMode}
      />
    </div>
  );
};

const ControlPanel = memo(({ sharedState, onUpdate }) => {
  console.log('ControlPanel rendered');
  
  return (
    <div className="control-panel">
      <h4>Controls</h4>
      
      <div className="control-group">
        <label>Filter:</label>
        <select 
          value={sharedState.filter}
          onChange={(e) => onUpdate({ filter: e.target.value })}
        >
          <option value="all">All</option>
          <option value="A">Category A</option>
          <option value="B">Category B</option>
          <option value="C">Category C</option>
        </select>
      </div>

      <div className="control-group">
        <label>Sort By:</label>
        <select 
          value={sharedState.sortBy}
          onChange={(e) => onUpdate({ sortBy: e.target.value })}
        >
          <option value="name">Name</option>
          <option value="category">Category</option>
        </select>
      </div>

      <div className="control-group">
        <label>View Mode:</label>
        <button 
          className={sharedState.viewMode === 'grid' ? 'active' : ''}
          onClick={() => onUpdate({ viewMode: 'grid' })}
        >
          Grid
        </button>
        <button 
          className={sharedState.viewMode === 'list' ? 'active' : ''}
          onClick={() => onUpdate({ viewMode: 'list' })}
        >
          List
        </button>
      </div>
    </div>
  );
});

const DataDisplay = memo(({ data, viewMode }) => {
  console.log('DataDisplay rendered');
  
  if (viewMode === 'grid') {
    return (
      <div className="data-grid">
        {data.map(item => (
          <div key={item.id} className="data-item">
            <h4>{item.name}</h4>
            <p>{item.category}</p>
          </div>
        ))}
      </div>
    );
  }
  
  return (
    <div className="data-list">
      {data.map(item => (
        <div key={item.id} className="list-item">
          <strong>{item.name}</strong> - {item.category}
        </div>
      ))}
    </div>
  );
});

5.2 条件渲染与列表渲染

5.2.1 高级条件渲染模式

多层级条件渲染优化:

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

// 1. 多条件渲染决策树
const AdvancedConditionalRendering = () => {
  const [user, setUser] = useState({
    isAuthenticated: false,
    role: 'guest',
    permissions: [],
    subscription: null
  });

  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // 使用 useMemo 缓存复杂的条件逻辑
  const componentConfig = useMemo(() => {
    const config = {
      showDashboard: false,
      showAdminPanel: false,
      showPremiumFeatures: false,
      showLoginPrompt: false,
      showErrorMessage: false,
      userRole: 'guest',
      accessLevel: 0
    };

    // 访问控制逻辑
    if (error) {
      config.showErrorMessage = true;
      return config;
    }

    if (loading) {
      config.showLoadingState = true;
      return config;
    }

    if (!user.isAuthenticated) {
      config.showLoginPrompt = true;
      config.userRole = 'guest';
      config.accessLevel = 0;
      return config;
    }

    // 认证用户的权限判断
    config.userRole = user.role;
    
    switch (user.role) {
      case 'admin':
        config.showDashboard = true;
        config.showAdminPanel = true;
        config.accessLevel = 100;
        break;
        
      case 'moderator':
        config.showDashboard = true;
        config.accessLevel = 70;
        break;
        
      case 'premium':
        config.showDashboard = true;
        config.showPremiumFeatures = true;
        config.accessLevel = 50;
        break;
        
      case 'user':
        config.showDashboard = true;
        config.accessLevel = 30;
        break;
        
      default:
        config.accessLevel = 10;
    }

    // 订阅状态判断
    if (user.subscription === 'premium') {
      config.showPremiumFeatures = true;
    }

    return config;
  }, [user, loading, error]);

  return (
    <div className="conditional-rendering-demo">
      <h2>Advanced Conditional Rendering</h2>
      
      {/* 控制面板 */}
      <ControlPanel 
        user={user} 
        setUser={setUser}
        setLoading={setLoading}
        setError={setError}
      />

      {/* 基于配置的条件渲染 */}
      {componentConfig.showLoadingState && <LoadingSpinner />}
      
      {componentConfig.showErrorMessage && (
        <ErrorMessage error={error} onDismiss={() => setError(null)} />
      )}
      
      {componentConfig.showLoginPrompt && <LoginPrompt />}
      
      {componentConfig.showDashboard && (
        <Dashboard 
          accessLevel={componentConfig.accessLevel}
          showAdminPanel={componentConfig.showAdminPanel}
          showPremiumFeatures={componentConfig.showPremiumFeatures}
        />
      )}
    </div>
  );
};

// 2. 渲染组件库
const LoadingSpinner = () => (
  <div className="loading-spinner">
    <div className="spinner"></div>
    <p>Loading...</p>
  </div>
);

const ErrorMessage = ({ error, onDismiss }) => (
  <div className="error-message">
    <h3>Error</h3>
    <p>{error}</p>
    <button onClick={onDismiss}>Dismiss</button>
  </div>
);

const LoginPrompt = () => (
  <div className="login-prompt">
    <h2>Welcome, Guest!</h2>
    <p>Please log in to access the full features.</p>
    <button onClick={() => {
      // 模拟登录
    }}>Login</button>
  </div>
);

const Dashboard = ({ accessLevel, showAdminPanel, showPremiumFeatures }) => (
  <div className="dashboard">
    <h2>Dashboard</h2>
    <p>Access Level: {accessLevel}</p>
    
    <BasicFeatures />
    
    {showPremiumFeatures && <PremiumFeatures />}
    
    {showAdminPanel && <AdminPanel />}
  </div>
);

const BasicFeatures = () => (
  <div className="features">
    <h3>Basic Features</h3>
    <div className="feature-grid">
      <FeatureCard title="Profile" description="View and edit your profile" />
      <FeatureCard title="Settings" description="Manage your account settings" />
      <FeatureCard title="Help" description="Get help and support" />
    </div>
  </div>
);

const PremiumFeatures = () => (
  <div className="premium-features">
    <h3>Premium Features</h3>
    <div className="feature-grid">
      <FeatureCard title="Advanced Analytics" description="Deep insights into your data" />
      <FeatureCard title="Priority Support" description="Get help faster" />
      <FeatureCard title="Custom Themes" description="Personalize your experience" />
    </div>
  </div>
);

const AdminPanel = () => (
  <div className="admin-panel">
    <h3>Admin Panel</h3>
    <div className="admin-tools">
      <AdminTool title="User Management" icon="👥" />
      <AdminTool title="System Settings" icon="⚙️" />
      <AdminTool title="Analytics" icon="📊" />
      <AdminTool title="Security" icon="🔒" />
    </div>
  </div>
);

const FeatureCard = ({ title, description }) => (
  <div className="feature-card">
    <h4>{title}</h4>
    <p>{description}</p>
  </div>
);

const AdminTool = ({ title, icon }) => (
  <div className="admin-tool">
    <span className="tool-icon">{icon}</span>
    <span className="tool-title">{title}</span>
  </div>
);

const ControlPanel = ({ user, setUser, setLoading, setError }) => {
  const scenarios = {
    guest: { isAuthenticated: false, role: 'guest' },
    basicUser: { 
      isAuthenticated: true, 
      role: 'user', 
      permissions: ['read'],
      subscription: null 
    },
    premiumUser: { 
      isAuthenticated: true, 
      role: 'premium', 
      permissions: ['read', 'write'],
      subscription: 'premium' 
    },
    moderator: { 
      isAuthenticated: true, 
      role: 'moderator', 
      permissions: ['read', 'write', 'moderate'],
      subscription: 'premium' 
    },
    admin: { 
      isAuthenticated: true, 
      role: 'admin', 
      permissions: ['read', 'write', 'moderate', 'admin'],
      subscription: 'premium' 
    }
  };

  const simulateScenario = (scenarioName) => {
    setUser(scenarios[scenarioName]);
  };

  return (
    <div className="control-panel">
      <h3>User Simulation</h3>
      <div className="scenario-buttons">
        {Object.keys(scenarios).map(scenario => (
          <button 
            key={scenario}
            onClick={() => simulateScenario(scenario)}
            className={user.role === scenarios[scenario].role ? 'active' : ''}
          >
            {scenario.charAt(0).toUpperCase() + scenario.slice(1)}
          </button>
        ))}
      </div>
      
      <div className="state-controls">
        <button onClick={() => setLoading(!loading)}>
          Toggle Loading
        </button>
        <button onClick={() => setError('Network connection failed')}>
          Simulate Error
        </button>
      </div>
      
      <div className="current-state">
        <p>Current Role: {user.role}</p>
        <p>Authenticated: {user.isAuthenticated ? 'Yes' : 'No'}</p>
        <p>Permissions: {user.permissions.join(', ') || 'None'}</p>
        <p>Subscription: {user.subscription || 'None'}</p>
      </div>
    </div>
  );
};

5.2.2 高性能列表渲染

虚拟滚动与分页策略:

import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';

// 1. 虚拟滚动实现
const VirtualizedList = ({ 
  items, 
  itemHeight = 50, 
  containerHeight = 400,
  renderItem,
  bufferSize = 5 
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 计算可见范围
  const visibleRange = useMemo(() => {
    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const end = Math.min(items.length, start + visibleCount + bufferSize * 2);
    
    return { start, end };
  }, [scrollTop, itemHeight, containerHeight, items.length, bufferSize]);

  // 可见项
  const visibleItems = useMemo(() => {
    return items.slice(visibleRange.start, visibleRange.end).map((item, index) => ({
      item,
      index: visibleRange.start + index
    }));
  }, [items, visibleRange]);

  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);

  // 容器样式
  const containerStyle = {
    height: containerHeight,
    overflow: 'auto',
    position: 'relative'
  };

  // 内容容器样式
  const contentStyle = {
    height: items.length * itemHeight,
    position: 'relative'
  };

  // 项目容器样式
  const itemContainerStyle = {
    position: 'absolute',
    top: visibleRange.start * itemHeight,
    left: 0,
    right: 0
  };

  return (
    <div 
      ref={containerRef}
      style={containerStyle}
      onScroll={handleScroll}
      className="virtualized-list"
    >
      <div style={contentStyle}>
        <div style={itemContainerStyle}>
          {visibleItems.map(({ item, index }) => (
            <div
              key={item.id || index}
              style={{
                height: itemHeight,
                borderBottom: '1px solid #eee'
              }}
            >
              {renderItem(item, index)}
            </div>
          ))}
        </div>
      </div>
      
      {/* 滚动指示器 */}
      <div className="scroll-indicator">
        Showing {visibleRange.end - visibleRange.start} of {items.length} items
      </div>
    </div>
  );
};

// 2. 分页组件
const PaginatedList = ({ 
  items, 
  itemsPerPage = 20,
  renderItem,
  maxPageButtons = 5 
}) => {
  const [currentPage, setCurrentPage] = useState(1);

  const totalPages = Math.ceil(items.length / itemsPerPage);
  
  const paginatedItems = useMemo(() => {
    const startIndex = (currentPage - 1) * itemsPerPage;
    const endIndex = startIndex + itemsPerPage;
    return items.slice(startIndex, endIndex);
  }, [items, currentPage, itemsPerPage]);

  const goToPage = useCallback((page) => {
    if (page >= 1 && page <= totalPages) {
      setCurrentPage(page);
    }
  }, [totalPages]);

  const goToPreviousPage = useCallback(() => {
    goToPage(currentPage - 1);
  }, [currentPage, goToPage]);

  const goToNextPage = useCallback(() => {
    goToPage(currentPage + 1);
  }, [currentPage, goToPage]);

  const pageNumbers = useMemo(() => {
    const pages = [];
    const halfMax = Math.floor(maxPageButtons / 2);
    
    let startPage = Math.max(1, currentPage - halfMax);
    let endPage = Math.min(totalPages, startPage + maxPageButtons - 1);
    
    if (endPage - startPage < maxPageButtons - 1) {
      startPage = Math.max(1, endPage - maxPageButtons + 1);
    }
    
    for (let i = startPage; i <= endPage; i++) {
      pages.push(i);
    }
    
    return pages;
  }, [currentPage, totalPages, maxPageButtons]);

  return (
    <div className="paginated-list">
      <div className="list-content">
        {paginatedItems.map((item, index) => 
          renderItem(item, (currentPage - 1) * itemsPerPage + index)
        )}
      </div>
      
      {totalPages > 1 && (
        <div className="pagination">
          <button 
            onClick={goToPreviousPage}
            disabled={currentPage === 1}
          >
            Previous
          </button>
          
          {pageNumbers[0] > 1 && (
            <>
              <button onClick={() => goToPage(1)}>1</button>
              {pageNumbers[0] > 2 && <span className="ellipsis">...</span>}
            </>
          )}
          
          {pageNumbers.map(page => (
            <button
              key={page}
              onClick={() => goToPage(page)}
              className={page === currentPage ? 'active' : ''}
            >
              {page}
            </button>
          ))}
          
          {pageNumbers[pageNumbers.length - 1] < totalPages && (
            <>
              {pageNumbers[pageNumbers.length - 1] < totalPages - 1 && (
                <span className="ellipsis">...</span>
              )}
              <button onClick={() => goToPage(totalPages)}>
                {totalPages}
              </button>
            </>
          )}
          
          <button 
            onClick={goToNextPage}
            disabled={currentPage === totalPages}
          >
            Next
          </button>
        </div>
      )}
      
      <div className="page-info">
        Page {currentPage} of {totalPages} | {items.length} total items
      </div>
    </div>
  );
};

// 3. 无限滚动组件
const InfiniteScrollList = ({ 
  items, 
  renderItem,
  onLoadMore,
  hasMore = true,
  loading = false,
  threshold = 100 
}) => {
  const observerRef = useRef();
  const lastItemRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const target = entries[0];
        if (target.isIntersecting && hasMore && !loading) {
          onLoadMore();
        }
      },
      {
        threshold: 0.1,
        rootMargin: `${threshold}px`
      }
    );

    if (lastItemRef.current) {
      observer.observe(lastItemRef.current);
    }

    observerRef.current = observer;

    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, [hasMore, loading, onLoadMore, threshold]);

  return (
    <div className="infinite-scroll-list">
      {items.map((item, index) => {
        const isLast = index === items.length - 1;
        
        return (
          <div
            key={item.id || index}
            ref={isLast ? lastItemRef : null}
            className="list-item"
          >
            {renderItem(item, index)}
          </div>
        );
      })}
      
      {loading && (
        <div className="loading-indicator">
          <div className="spinner"></div>
          <p>Loading more items...</p>
        </div>
      )}
      
      {!hasMore && items.length > 0 && (
        <div className="end-indicator">
          <p>No more items to load</p>
        </div>
      )}
    </div>
  );
};

// 4. 搜索和过滤列表
const FilteredList = ({ 
  items, 
  renderItem,
  searchFields = ['name'],
  filterOptions = {}
}) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [filters, setFilters] = useState({});
  const [sortBy, setSortBy] = useState('name');
  const [sortOrder, setSortOrder] = useState('asc');

  const filteredAndSortedItems = useMemo(() => {
    let filtered = items;

    // 搜索过滤
    if (searchTerm) {
      filtered = filtered.filter(item =>
        searchFields.some(field => {
          const value = item[field];
          return value && 
                 typeof value === 'string' && 
                 value.toLowerCase().includes(searchTerm.toLowerCase());
        })
      );
    }

    // 条件过滤
    Object.entries(filters).forEach(([key, value]) => {
      if (value && value !== 'all') {
        filtered = filtered.filter(item => item[key] === value);
      }
    });

    // 排序
    filtered.sort((a, b) => {
      let aVal = a[sortBy];
      let bVal = b[sortBy];

      if (typeof aVal === 'string') {
        aVal = aVal.toLowerCase();
        bVal = bVal.toLowerCase();
      }

      const comparison = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
      return sortOrder === 'asc' ? comparison : -comparison;
    });

    return filtered;
  }, [items, searchTerm, filters, sortBy, sortOrder, searchFields]);

  const handleFilterChange = useCallback((filterKey, value) => {
    setFilters(prev => ({
      ...prev,
      [filterKey]: value
    }));
  }, []);

  const handleSort = useCallback((field) => {
    if (sortBy === field) {
      setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
    } else {
      setSortBy(field);
      setSortOrder('asc');
    }
  }, [sortBy]);

  return (
    <div className="filtered-list">
      {/* 搜索栏 */}
      <div className="search-bar">
        <input
          type="text"
          placeholder="Search..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="search-input"
        />
        {searchTerm && (
          <button 
            onClick={() => setSearchTerm('')}
            className="clear-search"
          >
            Clear
          </button>
        )}
      </div>

      {/* 过滤器 */}
      <div className="filters">
        {Object.entries(filterOptions).map(([key, options]) => (
          <div key={key} className="filter-group">
            <label>{key}:</label>
            <select
              value={filters[key] || 'all'}
              onChange={(e) => handleFilterChange(key, e.target.value)}
            >
              <option value="all">All</option>
              {options.map(option => (
                <option key={option} value={option}>
                  {option}
                </option>
              ))}
            </select>
          </div>
        ))}
      </div>

      {/* 排序 */}
      <div className="sort-controls">
        {Object.keys(items[0] || {}).map(field => (
          <button
            key={field}
            onClick={() => handleSort(field)}
            className={sortBy === field ? 'active' : ''}
          >
            {field}
            {sortBy === field && (
              <span>{sortOrder === 'asc' ? '↑' : '↓'}</span>
            )}
          </button>
        ))}
      </div>

      {/* 结果统计 */}
      <div className="results-info">
        Showing {filteredAndSortedItems.length} of {items.length} items
      </div>

      {/* 列表内容 */}
      <div className="list-content">
        {filteredAndSortedItems.map((item, index) => 
          renderItem(item, index)
        )}
      </div>
    </div>
  );
};

// 5. 使用示例和演示
const ListRenderingDemo = () => {
  // 生成大量测试数据
  const [data, setData] = useState(() => 
    Array.from({ length: 10000 }, (_, i) => ({
      id: i + 1,
      name: `Item ${i + 1}`,
      category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)],
      value: Math.floor(Math.random() * 100),
      date: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
      status: ['active', 'inactive', 'pending'][Math.floor(Math.random() * 3)]
    }))
  );

  const [renderingMode, setRenderingMode] = useState('virtual');
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const renderItem = useCallback((item, index) => (
    <div className="item-card">
      <div className="item-header">
        <h4>{item.name}</h4>
        <span className={`status ${item.status}`}>{item.status}</span>
      </div>
      <div className="item-details">
        <p>Category: {item.category}</p>
        <p>Value: {item.value}</p>
        <p>Date: {new Date(item.date).toLocaleDateString()}</p>
      </div>
    </div>
  ), []);

  const handleLoadMore = useCallback(() => {
    if (isLoading || !hasMore) return;

    setIsLoading(true);
    
    // 模拟异步加载
    setTimeout(() => {
      setData(prev => {
        const newItems = Array.from({ length: 50 }, (_, i) => ({
          id: prev.length + i + 1,
          name: `Item ${prev.length + i + 1}`,
          category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)],
          value: Math.floor(Math.random() * 100),
          date: new Date().toISOString(),
          status: ['active', 'inactive', 'pending'][Math.floor(Math.random() * 3)]
        }));

        if (prev.length + 50 > 20000) {
          setHasMore(false);
        }

        return [...prev, ...newItems];
      });
      
      setIsLoading(false);
    }, 1000);
  }, [isLoading, hasMore]);

  const categories = useMemo(() => 
    [...new Set(data.map(item => item.category))],
    [data]
  );

  const statuses = useMemo(() => 
    [...new Set(data.map(item => item.status))],
    [data]
  );

  return (
    <div className="list-rendering-demo">
      <h2>Advanced List Rendering Demo</h2>
      
      <div className="mode-selector">
        <label>Rendering Mode:</label>
        <select 
          value={renderingMode} 
          onChange={(e) => setRenderingMode(e.target.value)}
        >
          <option value="virtual">Virtual Scrolling</option>
          <option value="paginated">Pagination</option>
          <option value="infinite">Infinite Scroll</option>
          <option value="filtered">Filtered List</option>
        </select>
      </div>

      <div className="rendering-container">
        {renderingMode === 'virtual' && (
          <VirtualizedList
            items={data}
            itemHeight={120}
            containerHeight={500}
            renderItem={renderItem}
          />
        )}

        {renderingMode === 'paginated' && (
          <PaginatedList
            items={data}
            itemsPerPage={20}
            renderItem={renderItem}
          />
        )}

        {renderingMode === 'infinite' && (
          <InfiniteScrollList
            items={data.slice(0, 500)} // 限制初始显示数量
            renderItem={renderItem}
            onLoadMore={handleLoadMore}
            hasMore={hasMore}
            loading={isLoading}
          />
        )}

        {renderingMode === 'filtered' && (
          <FilteredList
            items={data}
            renderItem={renderItem}
            searchFields={['name', 'category']}
            filterOptions={{
              category: categories,
              status: statuses
            }}
          />
        )}
      </div>

      <div className="performance-info">
        <p>Total Items: {data.length}</p>
        <p>Current Mode: {renderingMode}</p>
      </div>
    </div>
  );
};

通过这些渲染优化技术,你可以构建出高性能、流畅的用户界面,即使处理大量数据也能保持良好的用户体验。

5.3 事件处理与用户交互

5.3.1 高级事件处理模式

事件委托与优化:

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

// 1. 事件委托实现
const EventDelegationDemo = () => {
  const [selectedItem, setSelectedItem] = useState(null);
  const [actions, setActions] = useState([]);
  const containerRef = useRef(null);

  const handleClick = useCallback((e) => {
    // 使用事件委托处理子元素点击
    const target = e.target;
    
    if (target.matches('.item-card')) {
      const itemId = target.dataset.itemId;
      setSelectedItem(itemId);
      setActions(prev => [...prev, `Clicked item ${itemId}`]);
    } else if (target.matches('.action-button')) {
      const action = target.dataset.action;
      setActions(prev => [...prev, `Action: ${action}`]);
    }
  }, []);

  useEffect(() => {
    const container = containerRef.current;
    if (container) {
      container.addEventListener('click', handleClick);
      
      return () => {
        container.removeEventListener('click', handleClick);
      };
    }
  }, [handleClick]);

  const items = Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `Item ${i + 1}`,
    value: Math.random() * 100
  }));

  return (
    <div className="event-delegation-demo">
      <h2>Event Delegation Demo</h2>
      
      <div className="actions-log">
        <h4>Actions Log:</h4>
        {actions.slice(-5).map((action, index) => (
          <div key={index}>{action}</div>
        ))}
      </div>

      <div 
        ref={containerRef}
        className="items-container"
      >
        {items.map(item => (
          <div
            key={item.id}
            data-item-id={item.id}
            className="item-card"
          >
            <h4>{item.name}</h4>
            <p>Value: {item.value.toFixed(2)}</p>
            <div className="item-actions">
              <button 
                data-action={`edit-${item.id}`}
                className="action-button"
              >
                Edit
              </button>
              <button 
                data-action={`delete-${item.id}`}
                className="action-button"
              >
                Delete
              </button>
            </div>
          </div>
        ))}
      </div>

      {selectedItem && (
        <div className="selected-info">
          Selected Item: {selectedItem}
          <button onClick={() => setSelectedItem(null)}>
            Clear Selection
          </button>
        </div>
      )}
    </div>
  );
};

// 2. 防抖与节流事件
const DebounceThrottleDemo = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
  const [throttledValue, setThrottledValue] = useState(0);
  const [immediateValue, setImmediateValue] = useState(0);
  
  const [searchResults, setSearchResults] = useState([]);
  const [requestCount, setRequestCount] = useState(0);

  // 防抖 Hook
  const useDebounce = (value, delay) => {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      return () => {
        clearTimeout(handler);
      };
    }, [value, delay]);

    return debouncedValue;
  };

  // 节流 Hook
  const useThrottle = (value, delay) => {
    const [throttledValue, setThrottledValue] = useState(value);
    const lastExecuted = useRef(Date.now());

    useEffect(() => {
      const handler = setTimeout(() => {
        if (Date.now() - lastExecuted.current >= delay) {
          setThrottledValue(value);
          lastExecuted.current = Date.now();
        }
      }, delay - (Date.now() - lastExecuted.current));

      return () => {
        clearTimeout(handler);
      };
    }, [value, delay]);

    return throttledValue;
  };

  // 搜索 API 模拟
  const searchAPI = useCallback(async (term) => {
    if (!term.trim()) {
      setSearchResults([]);
      return;
    }

    setRequestCount(prev => prev + 1);
    
    // 模拟 API 延迟
    await new Promise(resolve => setTimeout(resolve, 500));
    
    const mockResults = Array.from({ length: 5 }, (_, i) => ({
      id: Date.now() + i,
      title: `${term} Result ${i + 1}`,
      description: `Description for ${term} result ${i + 1}`
    }));
    
    setSearchResults(mockResults);
  }, []);

  const debouncedTerm = useDebounce(searchTerm, 500);
  
  useEffect(() => {
    searchAPI(debouncedTerm);
  }, [debouncedTerm, searchAPI]);

  const handleMouseMove = useCallback((e) => {
    setImmediateValue(e.clientX);
  }, []);

  const throttledMouseMove = useThrottle(handleMouseMove, 100);

  const handleScroll = useCallback(() => {
    setThrottledValue(window.scrollY);
  }, []);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);

  return (
    <div className="debounce-throttle-demo">
      <h2>Debounce & Throttle Demo</h2>
      
      <div className="demo-section">
        <h3>Debounced Search</h3>
        <input
          type="text"
          placeholder="Search (debounced by 500ms)"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <p>Current: {searchTerm}</p>
        <p>Debounced: {debouncedTerm}</p>
        <p>API Requests: {requestCount}</p>
        
        {searchResults.length > 0 && (
          <div className="search-results">
            {searchResults.map(result => (
              <div key={result.id} className="result-item">
                <h4>{result.title}</h4>
                <p>{result.description}</p>
              </div>
            ))}
          </div>
        )}
      </div>

      <div className="demo-section">
        <h3>Throttled Scroll</h3>
        <p>Scroll the page to see throttled values</p>
        <p>Throttled Scroll Y: {throttledValue}</p>
        <p>Immediate Mouse X: <span 
          onMouseMove={throttledMouseMove}
          className="mouse-tracker"
        >
          Move mouse here
        </span> {immediateValue}</p>
      </div>

      <div className="demo-section">
        <h3>Performance Comparison</h3>
        <div className="comparison">
          <div className="comparison-item">
            <h4>Without Optimization</h4>
            <input 
              onChange={(e) => searchAPI(e.target.value)}
              placeholder="Immediate search"
            />
          </div>
          
          <div className="comparison-item">
            <h4>With Debounce</h4>
            <input
              value={searchTerm}
              onChange={(e) => setSearchTerm(e.target.value)}
              placeholder="Debounced search"
            />
          </div>
        </div>
      </div>
    </div>
  );
};

// 3. 复合事件处理
const ComplexInteractionDemo = () => {
  const [gesture, setGesture] = useState({
    type: null,
    startX: 0,
    startY: 0,
    currentX: 0,
    currentY: 0,
    distance: 0,
    duration: 0,
    velocity: 0
  });

  const [isDragging, setIsDragging] = useState(false);
  const [isLongPress, setIsLongPress] = useState(false);
  const [taps, setTaps] = useState([]);
  const [pinchDistance, setPinchDistance] = useState(0);

  const longPressTimer = useRef(null);
  const dragStartPos = useRef({ x: 0, y: 0 });
  const dragStartTime = useRef(0);

  const handleMouseDown = useCallback((e) => {
    const { clientX, clientY } = e.touches ? e.touches[0] : e;
    
    dragStartPos.current = { x: clientX, y: clientY };
    dragStartTime.current = Date.now();
    
    setIsDragging(true);
    
    // 长按检测
    longPressTimer.current = setTimeout(() => {
      setIsLongPress(true);
      setGesture(prev => ({
        ...prev,
        type: 'longpress',
        startX: clientX,
        startY: clientY,
        duration: 500
      }));
    }, 500);
  }, []);

  const handleMouseMove = useCallback((e) => {
    if (!isDragging) return;
    
    const { clientX, clientY } = e.touches ? e.touches[0] : e;
    const deltaX = clientX - dragStartPos.current.x;
    const deltaY = clientY - dragStartPos.current.y;
    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    
    // 如果移动距离超过阈值,取消长按
    if (distance > 10 && longPressTimer.current) {
      clearTimeout(longPressTimer.current);
      setIsLongPress(false);
    }
    
    setGesture(prev => ({
      ...prev,
      type: 'drag',
      currentX: clientX,
      currentY: clientY,
      distance,
      duration: Date.now() - dragStartTime.current,
      velocity: distance / (Date.now() - dragStartTime.current) * 1000
    }));
  }, [isDragging]);

  const handleMouseUp = useCallback((e) => {
    if (!isDragging) return;
    
    const { clientX, clientY } = e.changedTouches ? e.changedTouches[0] : e;
    const deltaX = clientX - dragStartPos.current.x;
    const deltaY = clientY - dragStartPos.current.y;
    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    const duration = Date.now() - dragStartTime.current;
    
    // 清除长按定时器
    if (longPressTimer.current) {
      clearTimeout(longPressTimer.current);
    }
    
    // 判断手势类型
    let gestureType = 'tap';
    if (isLongPress) {
      gestureType = 'longpress';
    } else if (duration < 200 && distance < 10) {
      gestureType = 'tap';
      const now = Date.now();
      setTaps(prev => [...prev.slice(-2), now]);
      
      // 检测双击
      if (taps.length >= 2 && now - taps[taps.length - 2] < 300) {
        gestureType = 'doubletap';
      }
    } else if (distance > 50 && duration < 500) {
      gestureType = 'swipe';
    } else {
      gestureType = 'drag';
    }
    
    setGesture({
      type: gestureType,
      startX: dragStartPos.current.x,
      startY: dragStartPos.current.y,
      currentX: clientX,
      currentY: clientY,
      distance,
      duration,
      velocity: distance / duration * 1000
    });
    
    setIsDragging(false);
    setIsLongPress(false);
  }, [isDragging, isLongPress, taps]);

  // 双指缩放处理
  const handleTouchMove = useCallback((e) => {
    if (e.touches.length === 2) {
      const touch1 = e.touches[0];
      const touch2 = e.touches[1];
      
      const distance = Math.sqrt(
        Math.pow(touch2.clientX - touch1.clientX, 2) +
        Math.pow(touch2.clientY - touch1.clientY, 2)
      );
      
      setPinchDistance(distance);
    }
  }, []);

  useEffect(() => {
    const element = document.querySelector('.gesture-area');
    
    if (element) {
      element.addEventListener('mousedown', handleMouseDown);
      element.addEventListener('mousemove', handleMouseMove);
      element.addEventListener('mouseup', handleMouseUp);
      element.addEventListener('touchstart', handleMouseDown);
      element.addEventListener('touchmove', (e) => {
        handleMouseMove(e);
        handleTouchMove(e);
      });
      element.addEventListener('touchend', handleMouseUp);
      
      return () => {
        element.removeEventListener('mousedown', handleMouseDown);
        element.removeEventListener('mousemove', handleMouseMove);
        element.removeEventListener('mouseup', handleMouseUp);
        element.removeEventListener('touchstart', handleMouseDown);
        element.removeEventListener('touchmove', handleMouseMove);
        element.removeEventListener('touchend', handleMouseUp);
      };
    }
  }, [handleMouseDown, handleMouseMove, handleMouseUp, handleTouchMove]);

  return (
    <div className="complex-interaction-demo">
      <h2>Complex Interaction Demo</h2>
      
      <div className="gesture-info">
        <h3>Gesture Information</h3>
        <div className="gesture-data">
          <p><strong>Type:</strong> {gesture.type}</p>
          <p><strong>Start:</strong> ({gesture.startX}, {gesture.startY})</p>
          <p><strong>Current:</strong> ({gesture.currentX}, {gesture.currentY})</p>
          <p><strong>Distance:</strong> {gesture.distance.toFixed(2)}px</p>
          <p><strong>Duration:</strong> {gesture.duration}ms</p>
          <p><strong>Velocity:</strong> {gesture.velocity.toFixed(2)}px/s</p>
          {pinchDistance > 0 && <p><strong>Pinch:</strong> {pinchDistance.toFixed(2)}px</p>}
        </div>
      </div>

      <div 
        className="gesture-area"
        style={{
          width: '400px',
          height: '300px',
          border: '2px solid #007bff',
          borderRadius: '8px',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          cursor: 'pointer',
          userSelect: 'none',
          backgroundColor: isDragging ? '#e3f2fd' : '#f8f9fa'
        }}
      >
        <div style={{ textAlign: 'center' }}>
          <h4>Gesture Area</h4>
          <p>Try: Tap, Double Tap, Long Press, Drag, Swipe</p>
          <p>Pinch to zoom (on touch devices)</p>
          {isLongPress && <p style={{ color: 'red' }}>Long Press Detected!</p>}
          {isDragging && <p style={{ color: 'blue' }}>Dragging...</p>}
        </div>
      </div>

      <div className="interaction-hints">
        <h3>Interaction Types</h3>
        <ul>
          <li><strong>Tap:</strong> Quick touch and release within 200ms</li>
          <li><strong>Double Tap:</strong> Two taps within 300ms</li>
          <li><strong>Long Press:</strong> Hold for 500ms</li>
          <li><strong>Drag:</strong> Move finger with distance > 10px</li>
          <li><strong>Swipe:</strong> Fast drag with distance > 50px</li>
          <li><strong>Pinch:</strong> Two finger pinch gesture</li>
        </ul>
      </div>
    </div>
  );
};

// 4. 键盘快捷键处理
const KeyboardShortcutsDemo = () => {
  const [shortcuts, setShortcuts] = useState([]);
  const [isCtrlPressed, setIsCtrlPressed] = useState(false);
  const [isShiftPressed, setIsShiftPressed] = useState(false);

  const addShortcut = useCallback((shortcut) => {
    setShortcuts(prev => [...prev.slice(-9), shortcut]);
  }, []);

  const handleKeyDown = useCallback((e) => {
    // 更新修饰键状态
    if (e.key === 'Control') setIsCtrlPressed(true);
    if (e.key === 'Shift') setIsShiftPressed(true);

    // 构建快捷键字符串
    const parts = [];
    if (e.ctrlKey) parts.push('Ctrl');
    if (e.shiftKey) parts.push('Shift');
    if (e.altKey) parts.push('Alt');
    if (e.metaKey) parts.push('Meta');
    parts.push(e.key);

    const shortcutString = parts.join('+');
    addShortcut(shortcutString);

    // 常用快捷键处理
    switch (shortcutString) {
      case 'Ctrl+S':
        e.preventDefault();
        console.log('Save action triggered');
        break;
      case 'Ctrl+C':
        console.log('Copy action triggered');
        break;
      case 'Ctrl+V':
        console.log('Paste action triggered');
        break;
      case 'Ctrl+Z':
        console.log('Undo action triggered');
        break;
      case 'Ctrl+Y':
        console.log('Redo action triggered');
        break;
      case 'Escape':
        console.log('Cancel action triggered');
        break;
      case 'F5':
        e.preventDefault();
        console.log('Refresh action triggered');
        break;
      default:
        // 其他快捷键处理
        break;
    }
  }, [addShortcut]);

  const handleKeyUp = useCallback((e) => {
    if (e.key === 'Control') setIsCtrlPressed(false);
    if (e.key === 'Shift') setIsShiftPressed(false);
  }, []);

  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  }, [handleKeyDown, handleKeyUp]);

  return (
    <div className="keyboard-shortcuts-demo">
      <h2>Keyboard Shortcuts Demo</h2>
      
      <div className="modifier-keys">
        <h3>Modifier Keys</h3>
        <div className={`key-indicator ${isCtrlPressed ? 'active' : ''}`}>
          Ctrl: {isCtrlPressed ? 'Pressed' : 'Released'}
        </div>
        <div className={`key-indicator ${isShiftPressed ? 'active' : ''}`}>
          Shift: {isShiftPressed ? 'Pressed' : 'Released'}
        </div>
      </div>

      <div className="shortcuts-log">
        <h3>Recent Shortcuts</h3>
        {shortcuts.map((shortcut, index) => (
          <div key={index} className="shortcut-item">
            {shortcut}
          </div>
        ))}
      </div>

      <div className="shortcut-cheatsheet">
        <h3>Common Shortcuts</h3>
        <div className="shortcut-grid">
          <div className="shortcut-item">
            <kbd>Ctrl</kbd> + <kbd>S</kbd>
            <span>Save</span>
          </div>
          <div className="shortcut-item">
            <kbd>Ctrl</kbd> + <kbd>C</kbd>
            <span>Copy</span>
          </div>
          <div className="shortcut-item">
            <kbd>Ctrl</kbd> + <kbd>V</kbd>
            <span>Paste</span>
          </div>
          <div className="shortcut-item">
            <kbd>Ctrl</kbd> + <kbd>Z</kbd>
            <span>Undo</span>
          </div>
          <div className="shortcut-item">
            <kbd>Ctrl</kbd> + <kbd>Y</kbd>
            <span>Redo</span>
          </div>
          <div className="shortcut-item">
            <kbd>Escape</kbd>
            <span>Cancel</span>
          </div>
        </div>
      </div>
    </div>
  );
};

5.3.2 响应式设计与自适应交互

响应式布局与交互优化:

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

// 1. 响应式 Hook
const useResponsive = () => {
  const [windowSize, setWindowSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 1200,
    height: typeof window !== 'undefined' ? window.innerHeight : 800
  });

  const [deviceType, setDeviceType] = useState('desktop');

  useEffect(() => {
    const handleResize = () => {
      const width = window.innerWidth;
      const height = window.innerHeight;
      
      setWindowSize({ width, height });
      
      // 设备类型判断
      if (width < 768) {
        setDeviceType('mobile');
      } else if (width < 1024) {
        setDeviceType('tablet');
      } else {
        setDeviceType('desktop');
      }
    };

    handleResize();
    window.addEventListener('resize', handleResize);
    
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  const isMobile = deviceType === 'mobile';
  const isTablet = deviceType === 'tablet';
  const isDesktop = deviceType === 'desktop';
  
  return {
    windowSize,
    deviceType,
    isMobile,
    isTablet,
    isDesktop
  };
};

// 2. 响应式网格布局
const ResponsiveGrid = ({ items, renderItem }) => {
  const { windowSize, deviceType } = useResponsive();

  const gridConfig = useMemo(() => {
    switch (deviceType) {
      case 'mobile':
        return { columns: 1, gap: 16, itemHeight: 200 };
      case 'tablet':
        return { columns: 2, gap: 20, itemHeight: 250 };
      case 'desktop':
        return { columns: windowSize.width >= 1440 ? 4 : 3, gap: 24, itemHeight: 300 };
      default:
        return { columns: 3, gap: 24, itemHeight: 300 };
    }
  }, [deviceType, windowSize.width]);

  const gridStyle = useMemo(() => ({
    display: 'grid',
    gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`,
    gap: `${gridConfig.gap}px`,
    padding: `${gridConfig.gap}px`
  }), [gridConfig]);

  return (
    <div className="responsive-grid" style={gridStyle}>
      {items.map((item, index) => (
        <div 
          key={item.id || index} 
          style={{ height: `${gridConfig.itemHeight}px` }}
        >
          {renderItem(item, index, deviceType)}
        </div>
      ))}
    </div>
  );
};

// 3. 自适应交互组件
const AdaptiveUI = () => {
  const { deviceType, isMobile, isTablet, isDesktop } = useResponsive();
  const [selectedView, setSelectedView] = useState('grid');
  const [sidebarOpen, setSidebarOpen] = useState(false);

  // 响应式视图切换
  const viewOptions = useMemo(() => {
    const baseOptions = [
      { id: 'grid', label: 'Grid View', icon: '⊞' },
      { id: 'list', label: 'List View', icon: '☰' }
    ];
    
    if (!isMobile) {
      baseOptions.push(
        { id: 'table', label: 'Table View', icon: '⊟' },
        { id: 'card', label: 'Card View', icon: '▦' }
      );
    }
    
    return baseOptions;
  }, [isMobile]);

  // 生成测试数据
  const items = useMemo(() => 
    Array.from({ length: 20 }, (_, i) => ({
      id: i + 1,
      title: `Item ${i + 1}`,
      description: `Description for item ${i + 1}`,
      category: ['Design', 'Development', 'Marketing', 'Sales'][i % 4],
      status: ['active', 'pending', 'completed'][i % 3],
      value: Math.floor(Math.random() * 100),
      date: new Date(Date.now() - Math.random() * 10000000000).toLocaleDateString()
    }))
  , []);

  // 渲染函数
  const renderItem = useCallback((item, index, device) => {
    switch (selectedView) {
      case 'grid':
        return <GridViewItem item={item} device={device} />;
      case 'list':
        return <ListViewItem item={item} device={device} />;
      case 'table':
        return <TableViewItem item={item} index={index} />;
      case 'card':
        return <CardViewItem item={item} />;
      default:
        return <GridViewItem item={item} device={device} />;
    }
  }, [selectedView]);

  const toggleSidebar = useCallback(() => {
    setSidebarOpen(!sidebarOpen);
  }, [sidebarOpen]);

  return (
    <div className={`adaptive-ui ${deviceType}`}>
      <header className="app-header">
        <div className="header-left">
          {isMobile && (
            <button 
              onClick={toggleSidebar}
              className="menu-toggle"
            >
              ☰
            </button>
          )}
          <h1>Adaptive UI Demo</h1>
        </div>
        
        <nav className="header-nav">
          {!isMobile && (
            <div className="view-switcher">
              {viewOptions.map(option => (
                <button
                  key={option.id}
                  onClick={() => setSelectedView(option.id)}
                  className={`view-button ${selectedView === option.id ? 'active' : ''}`}
                >
                  <span className="icon">{option.icon}</span>
                  <span className="label">{option.label}</span>
                </button>
              ))}
            </div>
          )}
          
          {isMobile && (
            <select
              value={selectedView}
              onChange={(e) => setSelectedView(e.target.value)}
              className="mobile-view-select"
            >
              {viewOptions.map(option => (
                <option key={option.id} value={option.id}>
                  {option.label}
                </option>
              ))}
            </select>
          )}
        </nav>
      </header>

      <div className="app-body">
        <aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
          <h3>Filters</h3>
          
          {/* 移动端关闭按钮 */}
          {isMobile && (
            <button 
              onClick={() => setSidebarOpen(false)}
              className="sidebar-close"
            >
              ×
            </button>
          )}
          
          <div className="filter-group">
            <label>Category:</label>
            <select>
              <option value="">All Categories</option>
              <option value="Design">Design</option>
              <option value="Development">Development</option>
              <option value="Marketing">Marketing</option>
              <option value="Sales">Sales</option>
            </select>
          </div>
          
          <div className="filter-group">
            <label>Status:</label>
            <select>
              <option value="">All Status</option>
              <option value="active">Active</option>
              <option value="pending">Pending</option>
              <option value="completed">Completed</option>
            </select>
          </div>

          {!isMobile && (
            <div className="device-info">
              <h4>Device Info</h4>
              <p>Device: {deviceType}</p>
              <p>Screen: {isMobile ? 'Small' : isTablet ? 'Medium' : 'Large'}</p>
            </div>
          )}
        </aside>

        <main className="content">
          {isMobile && (
            <button 
              onClick={() => setSidebarOpen(true)}
              className="filter-toggle"
              style={{ marginBottom: '16px' }}
            >
              🔍 Filters
            </button>
          )}
          
          <ResponsiveGrid 
            items={items}
            renderItem={renderItem}
          />
        </main>
      </div>

      {/* 移动端遮罩 */}
      {isMobile && sidebarOpen && (
        <div 
          className="overlay"
          onClick={() => setSidebarOpen(false)}
        />
      )}
    </div>
  );
};

// 4. 视图组件
const GridViewItem = ({ item, device }) => (
  <div className="grid-item">
    <h4>{item.title}</h4>
    <p>{!device || device === 'mobile' 
      ? item.description.slice(0, 50) + '...' 
      : item.description
    }</p>
    <span className={`status ${item.status}`}>{item.status}</span>
    <p className="category">{item.category}</p>
  </div>
);

const ListViewItem = ({ item, device }) => (
  <div className={`list-item ${device}`}>
    <div className="item-content">
      <div className="item-header">
        <h4>{item.title}</h4>
        <span className={`status ${item.status}`}>{item.status}</span>
      </div>
      <p>{item.description}</p>
      <div className="item-meta">
        <span className="category">{item.category}</span>
        <span className="date">{item.date}</span>
      </div>
    </div>
    {!device || device === 'desktop' ? (
      <div className="item-actions">
        <button>View</button>
        <button>Edit</button>
        <button>Delete</button>
      </div>
    ) : (
      <button className="action-menu">⋯</button>
    )}
  </div>
);

const TableViewItem = ({ item, index }) => (
  <tr className="table-item">
    <td>{index + 1}</td>
    <td>{item.title}</td>
    <td>{item.category}</td>
    <td>
      <span className={`status ${item.status}`}>{item.status}</span>
    </td>
    <td>{item.value}</td>
    <td>{item.date}</td>
    <td>
      <div className="table-actions">
        <button>👁</button>
        <button>✏️</button>
        <button>🗑️</button>
      </div>
    </td>
  </tr>
);

const CardViewItem = ({ item }) => (
  <div className="card-item">
    <div className="card-header">
      <h4>{item.title}</h4>
      <span className={`status ${item.status}`}>{item.status}</span>
    </div>
    <div className="card-body">
      <p>{item.description}</p>
      <div className="card-stats">
        <div className="stat">
          <label>Category</label>
          <span>{item.category}</span>
        </div>
        <div className="stat">
          <label>Value</label>
          <span>{item.value}</span>
        </div>
        <div className="stat">
          <label>Date</label>
          <span>{item.date}</span>
        </div>
      </div>
    </div>
    <div className="card-footer">
      <button>View Details</button>
      <button>Quick Action</button>
    </div>
  </div>
);

通过这些高级事件处理和响应式设计技术,你可以构建出适应各种设备和交互场景的用户界面,提供优秀的用户体验。

5.4 动画与过渡效果

5.4.1 CSS 过渡与动画

React 中的 CSS 动画实现:

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

// 1. CSS 过渡效果组件
const CSSTransitionDemo = () => {
  const [showBox, setShowBox] = useState(false);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [notification, setNotification] = useState(null);
  const [items, setItems] = useState([]);

  // 通知系统
  const showNotification = useCallback((message, type = 'info') => {
    const id = Date.now();
    const newNotification = { id, message, type };
    
    setNotification(newNotification);
    
    setTimeout(() => {
      setNotification(null);
    }, 3000);
  }, []);

  // 列表项操作
  const addItem = useCallback(() => {
    const id = Date.now();
    setItems(prev => [...prev, { id, name: `Item ${id}` }]);
  }, []);

  const removeItem = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);

  return (
    <div className="css-transition-demo">
      <h2>CSS Transitions Demo</h2>
      
      {/* 基础过渡效果 */}
      <section className="demo-section">
        <h3>Basic Transitions</h3>
        <button onClick={() => setShowBox(!showBox)}>
          Toggle Box
        </button>
        <div className={`transition-box ${showBox ? 'visible' : 'hidden'}`}>
          Smooth Transition Box
        </div>
      </section>

      {/* 模态框过渡 */}
      <section className="demo-section">
        <h3>Modal Transition</h3>
        <button onClick={() => setIsModalOpen(true)}>
          Open Modal
        </button>
        
        <div className={`modal-overlay ${isModalOpen ? 'open' : 'closed'}`}>
          <div className={`modal-content ${isModalOpen ? 'show' : 'hide'}`}>
            <h3>Modal Title</h3>
            <p>This is a modal with smooth transitions.</p>
            <div className="modal-actions">
              <button onClick={() => setIsModalOpen(false)}>
                Cancel
              </button>
              <button onClick={() => setIsModalOpen(false)}>
                Confirm
              </button>
            </div>
          </div>
        </div>
      </section>

      {/* 通知动画 */}
      <section className="demo-section">
        <h3>Notification Animations</h3>
        <div className="notification-buttons">
          <button onClick={() => showNotification('Success message!', 'success')}>
            Success
          </button>
          <button onClick={() => showNotification('Warning message!', 'warning')}>
            Warning
          </button>
          <button onClick={() => showNotification('Error message!', 'error')}>
            Error
          </button>
        </div>
        
        <div className="notification-container">
          {notification && (
            <div className={`notification ${notification.type} show`}>
              {notification.message}
            </div>
          )}
        </div>
      </section>

      {/* 列表动画 */}
      <section className="demo-section">
        <h3>List Animations</h3>
        <div className="list-controls">
          <button onClick={addItem}>Add Item</button>
          <button onClick={() => setItems([])}>Clear All</button>
        </div>
        
        <div className="animated-list">
          {items.map((item, index) => (
            <div 
              key={item.id}
              className="list-item show"
              style={{
                animationDelay: `${index * 0.1}s`
              }}
            >
              <span>{item.name}</span>
              <button 
                onClick={() => removeItem(item.id)}
                className="remove-btn"
              >
                ×
              </button>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
};

// 2. 自定义动画组件
const CustomAnimationComponent = () => {
  const [isAnimating, setIsAnimating] = useState(false);
  const [progress, setProgress] = useState(0);
  const animationRef = useRef(null);

  // 关键帧动画
  const triggerKeyframeAnimation = () => {
    setIsAnimating(true);
    
    if (animationRef.current) {
      animationRef.current.beginElement();
    }
    
    setTimeout(() => setIsAnimating(false), 2000);
  };

  // 进度条动画
  const startProgressAnimation = () => {
    setProgress(0);
    let currentProgress = 0;
    
    const interval = setInterval(() => {
      currentProgress += 2;
      setProgress(currentProgress);
      
      if (currentProgress >= 100) {
        clearInterval(interval);
      }
    }, 50);
  };

  return (
    <div className="custom-animation-demo">
      <h2>Custom Animations</h2>
      
      {/* SVG 动画 */}
      <section className="demo-section">
        <h3>SVG Animations</h3>
        <div className="svg-animation">
          <svg width="200" height="200" viewBox="0 0 200 200">
            {/* 定义动画 */}
            <defs>
              <animateTransform
                id="rotateAnimation"
                attributeName="transform"
                attributeType="XML"
                type="rotate"
                from="0 100 100"
                to="360 100 100"
                dur="2s"
                repeatCount="1"
              />
              
              <animate
                id="pulseAnimation"
                attributeName="r"
                values="40;60;40"
                dur="2s"
                repeatCount="1"
              />
            </defs>
            
            {/* 旋转的方块 */}
            <rect
              ref={animationRef}
              x="60"
              y="60"
              width="80"
              height="80"
              fill="#007bff"
              rx="8"
            />
            
            {/* 脉冲的圆 */}
            <circle
              cx="100"
              cy="100"
              r="40"
              fill="none"
              stroke="#28a745"
              strokeWidth="3"
            >
              <animate
                attributeName="r"
                values="40;60;40"
                dur="2s"
                repeatCount="indefinite"
              />
              <animate
                attributeName="opacity"
                values="1;0.3;1"
                dur="2s"
                repeatCount="indefinite"
              />
            </circle>
          </svg>
          
          <button onClick={triggerKeyframeAnimation}>
            Trigger SVG Animation
          </button>
        </div>
      </section>

      {/* 进度条动画 */}
      <section className="demo-section">
        <h3>Progress Bar Animation</h3>
        <div className="progress-container">
          <div 
            className="progress-bar"
            style={{ width: `${progress}%` }}
          >
            {progress}%
          </div>
        </div>
        <button onClick={startProgressAnimation}>
          Start Progress Animation
        </button>
      </section>

      {/* 复杂动画序列 */}
      <section className="demo-section">
        <h3>Animation Sequence</h3>
        <div className="animation-sequence">
          <div className="sequence-item" style={{ animationDelay: '0s' }}>
            Step 1
          </div>
          <div className="sequence-item" style={{ animationDelay: '0.5s' }}>
            Step 2
          </div>
          <div className="sequence-item" style={{ animationDelay: '1s' }}>
            Step 3
          </div>
          <div className="sequence-item" style={{ animationDelay: '1.5s' }}>
            Complete!
          </div>
        </div>
      </section>
    </div>
  );
};

// 3. 交互式动画组件
const InteractiveAnimationDemo = () => {
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  const [isHovered, setIsHovered] = useState(false);
  const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 });
  const [ripples, setRipples] = useState([]);
  
  const containerRef = useRef(null);

  // 鼠标跟踪
  const handleMouseMove = (e) => {
    if (containerRef.current) {
      const rect = containerRef.current.getBoundingClientRect();
      setMousePosition({
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
      });
    }
  };

  // 点击涟漪效果
  const handleClick = (e) => {
    if (containerRef.current) {
      const rect = containerRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      
      const ripple = {
        id: Date.now(),
        x,
        y
      };
      
      setRipples(prev => [...prev, ripple]);
      
      setTimeout(() => {
        setRipples(prev => prev.filter(r => r.id !== ripple.id));
      }, 1000);
    }
  };

  return (
    <div className="interactive-animation-demo">
      <h2>Interactive Animations</h2>
      
      {/* 鼠标跟随效果 */}
      <section className="demo-section">
        <h3>Mouse Following</h3>
        <div 
          className="mouse-follow-container"
          ref={containerRef}
          onMouseMove={handleMouseMove}
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
        >
          <div 
            className="mouse-follower"
            style={{
              left: `${mousePosition.x}px`,
              top: `${mousePosition.y}px`,
              opacity: isHovered ? 1 : 0
            }}
          />
          <div className="content">
            Move your mouse here!
          </div>
        </div>
      </section>

      {/* 点击涟漪效果 */}
      <section className="demo-section">
        <h3>Click Ripple Effect</h3>
        <div 
          className="ripple-container"
          onClick={handleClick}
        >
          {ripples.map(ripple => (
            <div
              key={ripple.id}
              className="ripple"
              style={{
                left: `${ripple.x}px`,
                top: `${ripple.y}px`
              }}
            />
          ))}
          <div className="content">
            Click anywhere to create ripples
          </div>
        </div>
      </section>

      {/* 拖拽动画 */}
      <section className="demo-section">
        <h3>Drag Animation</h3>
        <DraggableAnimation />
      </section>
    </div>
  );
};

// 可拖拽动画组件
const DraggableAnimation = () => {
  const [position, setPosition] = useState({ x: 50, y: 50 });
  const [isDragging, setIsDragging] = useState(false);
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
  
  const handleMouseDown = (e) => {
    setIsDragging(true);
    setDragStart({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };

  const handleMouseMove = (e) => {
    if (isDragging) {
      setPosition({
        x: e.clientX - dragStart.x,
        y: e.clientY - dragStart.y
      });
    }
  };

  const handleMouseUp = () => {
    setIsDragging(false);
  };

  useEffect(() => {
    if (isDragging) {
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      
      return () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
    }
  }, [isDragging, dragStart]);

  return (
    <div className="draggable-animation">
      <div
        className={`draggable-box ${isDragging ? 'dragging' : ''}`}
        style={{
          left: `${position.x}px`,
          top: `${position.y}px`,
          transform: isDragging ? 'scale(1.1)' : 'scale(1)',
          cursor: isDragging ? 'grabbing' : 'grab'
        }}
        onMouseDown={handleMouseDown}
      >
        Drag me!
      </div>
    </div>
  );
};

5.4.2 React 动画库集成

Framer Motion 高级动画:

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence, useAnimation } from 'framer-motion';

// 1. Framer Motion 基础动画
const FramerMotionDemo = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1', color: '#ff6b6b' },
    { id: 2, text: 'Item 2', color: '#4ecdc4' },
    { id: 3, text: 'Item 3', color: '#45b7d1' }
  ]);

  const addItem = () => {
    const newId = Math.max(...items.map(item => item.id), 0) + 1;
    const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7dc6f', '#bb8fce'];
    setItems([
      ...items,
      {
        id: newId,
        text: `Item ${newId}`,
        color: colors[Math.floor(Math.random() * colors.length)]
      }
    ]);
  };

  const removeItem = (id) => {
    setItems(items.filter(item => item.id !== id));
  };

  return (
    <div className="framer-motion-demo">
      <h2>Framer Motion Demo</h2>
      
      {/* 基础动画 */}
      <motion.section className="demo-section">
        <h3>Basic Animations</h3>
        
        <motion.button
          whileHover={{ scale: 1.05 }}
          whileTap={{ scale: 0.95 }}
          onClick={() => setIsOpen(!isOpen)}
        >
          Toggle Box
        </motion.button>

        <AnimatePresence>
          {isOpen && (
            <motion.div
              initial={{ opacity: 0, scale: 0.8 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.8 }}
              transition={{ duration: 0.3, ease: 'easeInOut' }}
              className="animated-box"
            >
              Animated Box with Framer Motion
            </motion.div>
          )}
        </AnimatePresence>
      </motion.section>

      {/* 手势动画 */}
      <motion.section className="demo-section">
        <h3>Gesture Animations</h3>
        
        <motion.div
          drag
          dragConstraints={{ left: 0, right: 300, top: 0, bottom: 200 }}
          dragElastic={0.2}
          whileDrag={{ scale: 1.1 }}
          className="draggable-motion-box"
        >
          Drag me around!
        </motion.div>

        <motion.button
          whileHover={{ 
            scale: 1.1,
            backgroundColor: '#007bff',
            color: 'white'
          }}
          whileTap={{ 
            scale: 0.9,
            backgroundColor: '#0056b3'
          }}
          transition={{ type: 'spring', stiffness: 400, damping: 17 }}
        >
          Hover & Tap Effects
        </motion.button>
      </motion.section>

      {/* 列表动画 */}
      <motion.section className="demo-section">
        <h3>List Animations</h3>
        
        <motion.button 
          onClick={addItem}
          whileHover={{ scale: 1.05 }}
          whileTap={{ scale: 0.95 }}
        >
          Add Item
        </motion.button>

        <motion.ul className="motion-list">
          <AnimatePresence>
            {items.map((item, index) => (
              <motion.li
                key={item.id}
                initial={{ 
                  opacity: 0, 
                  x: -50,
                  scale: 0.8
                }}
                animate={{ 
                  opacity: 1, 
                  x: 0,
                  scale: 1
                }}
                exit={{ 
                  opacity: 0, 
                  x: 50,
                  scale: 0.8
                }}
                transition={{
                  type: 'spring',
                  stiffness: 300,
                  damping: 30,
                  delay: index * 0.1
                }}
                layout
                whileHover={{ 
                  scale: 1.02,
                  backgroundColor: item.color,
                  color: 'white'
                }}
                style={{ borderLeft: `4px solid ${item.color}` }}
              >
                <span>{item.text}</span>
                <motion.button
                  whileHover={{ scale: 1.1 }}
                  whileTap={{ scale: 0.9 }}
                  onClick={() => removeItem(item.id)}
                  className="remove-motion-btn"
                >
                  ×
                </motion.button>
              </motion.li>
            ))}
          </AnimatePresence>
        </motion.ul>
      </motion.section>

      {/* 复杂动画序列 */}
      <ComplexAnimationSequence />
    </div>
  );
};

// 复杂动画序列组件
const ComplexAnimationSequence = () => {
  const controls = useAnimation();
  const [sequenceStep, setSequenceStep] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);

  const playSequence = async () => {
    setIsPlaying(true);
    
    // 步骤 1: 淡入
    await controls.start({
      opacity: 1,
      scale: 1,
      rotate: 0,
      transition: { duration: 0.8, ease: 'easeOut' }
    });
    setSequenceStep(1);
    
    // 步骤 2: 旋转
    await controls.start({
      rotate: 360,
      transition: { duration: 1, ease: 'easeInOut' }
    });
    setSequenceStep(2);
    
    // 步骤 3: 变形
    await controls.start({
      borderRadius: '50%',
      scale: 1.2,
      backgroundColor: '#4ecdc4',
      transition: { duration: 0.8, ease: 'easeInOut' }
    });
    setSequenceStep(3);
    
    // 步骤 4: 弹跳
    await controls.start({
      y: [0, -50, 0],
      transition: { 
        duration: 0.6,
        ease: 'easeOut',
        times: [0, 0.5, 1]
      }
    });
    setSequenceStep(4);
    
    // 步骤 5: 重置
    await controls.start({
      borderRadius: '8px',
      scale: 1,
      backgroundColor: '#007bff',
      y: 0,
      transition: { duration: 0.8, ease: 'easeInOut' }
    });
    setSequenceStep(0);
    setIsPlaying(false);
  };

  const resetAnimation = () => {
    controls.set({
      opacity: 0.3,
      scale: 0.8,
      rotate: 0
    });
    setSequenceStep(0);
    setIsPlaying(false);
  };

  return (
    <section className="demo-section">
      <h3>Animation Sequence</h3>
      
      <div className="sequence-controls">
        <button
          onClick={playSequence}
          disabled={isPlaying}
          whileHover={{ scale: 1.05 }}
          whileTap={{ scale: 0.95 }}
        >
          {isPlaying ? 'Playing...' : 'Play Sequence'}
        </button>
        
        <button
          onClick={resetAnimation}
          whileHover={{ scale: 1.05 }}
          whileTap={{ scale: 0.95 }}
        >
          Reset
        </button>
      </div>

      <div className="sequence-step">
        Current Step: {sequenceStep}/4
      </div>

      <motion.div
        animate={controls}
        initial={{
          opacity: 0.3,
          scale: 0.8,
          rotate: 0
        }}
        className="sequence-box"
      />

      <div className="sequence-description">
        <div className={`step ${sequenceStep >= 1 ? 'completed' : ''}`}>
          1. Fade In
        </div>
        <div className={`step ${sequenceStep >= 2 ? 'completed' : ''}`}>
          2. Rotate
        </div>
        <div className={`step ${sequenceStep >= 3 ? 'completed' : ''}`}>
          3. Transform
        </div>
        <div className={`step ${sequenceStep >= 4 ? 'completed' : ''}`}>
          4. Bounce
        </div>
        <div className={`step ${sequenceStep >= 5 ? 'completed' : ''}`}>
          5. Reset
        </div>
      </div>
    </section>
  );
};

// 响应式动画演示
const ResponsiveAnimationDemo = () => {
  const [windowSize, setWindowSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 1200,
    height: typeof window !== 'undefined' ? window.innerHeight : 800
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  const isMobile = windowSize.width < 768;
  const isTablet = windowSize.width >= 768 && windowSize.width < 1024;

  return (
    <section className="demo-section">
      <h3>Responsive Animations</h3>
      
      <div className="responsive-info">
        <p>Window Size: {windowSize.width} x {windowSize.height}</p>
        <p>Device: {isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop'}</p>
      </div>

      <motion.div
        initial={{ x: -200 }}
        animate={{ 
          x: isMobile ? 0 : isTablet ? -50 : -100,
          rotate: isMobile ? 0 : 5
        }}
        transition={{
          type: 'spring',
          stiffness: isMobile ? 200 : 400,
          damping: isMobile ? 20 : 30
        }}
        className="responsive-box"
      >
        Responsive Animation
      </motion.div>

      <motion.div
        variants={{
          mobile: { scale: 0.8, rotate: 0 },
          tablet: { scale: 1, rotate: 5 },
          desktop: { scale: 1.2, rotate: 10 }
        }}
        animate={isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop'}
        transition={{ duration: 0.5, ease: 'easeInOut' }}
        className="device-variant-box"
      >
        Device Variant
      </motion.div>
    </section>
  );
};

5.4.3 性能优化动画

高性能动画实现:

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

// 1. 使用 CSS Transform 和 Will-change
const PerformantAnimation = () => {
  const [isAnimating, setIsAnimating] = useState(false);
  const [scrollY, setScrollY] = useState(0);
  const containerRef = useRef(null);

  // 优化的滚动动画
  const optimizedScroll = useCallback(() => {
    requestAnimationFrame(() => {
      setScrollY(window.scrollY);
    });
  }, []);

  useEffect(() => {
    window.addEventListener('scroll', optimizedScroll);
    return () => window.removeEventListener('scroll', optimizedScroll);
  }, [optimizedScroll]);

  // GPU 加速动画
  const triggerPerformanceAnimation = () => {
    setIsAnimating(true);
    
    setTimeout(() => {
      setIsAnimating(false);
    }, 1000);
  };

  return (
    <div className="performant-animation">
      <h2>Performance Optimized Animations</h2>
      
      {/* GPU 加速动画 */}
      <section className="demo-section">
        <h3>GPU Accelerated Animation</h3>
        
        <div className="animation-grid">
          <div className={`gpu-box ${isAnimating ? 'animate' : ''}`}>
            Transform & Opacity (GPU)
          </div>
          
          <div className={`bad-box ${isAnimating ? 'animate' : ''}`}>
            Left & Top (CPU)
          </div>
        </div>
        
        <button onClick={triggerPerformanceAnimation}>
          Toggle Performance Animation
        </button>
        
        <div className="performance-tips">
          <h4>Performance Tips:</h4>
          <ul>
            <li>Use <code>transform</code> and <code>opacity</code> for smooth animations</li>
            <li>Avoid animating <code>width</code>, <code>height</code>, <code>left</code>, <code>top</code></li>
            <li>Use <code>will-change</code> property for elements that will animate</li>
            <li>Prefer <code>translateZ(0)</code> to force GPU layer</li>
          </ul>
        </div>
      </section>

      {/* 优化的滚动动画 */}
      <section className="demo-section">
        <h3>Optimized Scroll Animation</h3>
        
        <div 
          ref={containerRef}
          className="scroll-container"
          style={{ height: '400px', overflow: 'auto' }}
        >
          <div style={{ height: '2000px', position: 'relative' }}>
            <div 
              className="sticky-element"
              style={{
                position: 'sticky',
                top: '20px',
                transform: `translateY(${Math.min(scrollY * 0.5, 100)}px)`,
                willChange: 'transform'
              }}
            >
              Sticky Element with Smooth Motion
            </div>
            
            {/* 滚动内容 */}
            {Array.from({ length: 20 }, (_, i) => (
              <div key={i} className="scroll-content">
                Scroll Item {i + 1}
              </div>
            ))}
          </div>
        </div>
        
        <div className="scroll-info">
          Scroll Y: {scrollY}px
        </div>
      </section>

      {/* 防抖动画 */}
      <section className="demo-section">
        <h3>Debounced Animation</h3>
        
        <DebouncedAnimation />
      </section>
    </div>
  );
};

// 防抖动画组件
const DebouncedAnimation = () => {
  const [value, setValue] = useState('');
  const [debouncedValue, setDebouncedValue] = useState('');
  const [isAnimating, setIsAnimating] = useState(false);
  const timeoutRef = useRef(null);

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      setDebouncedValue(value);
      setIsAnimating(true);
      
      setTimeout(() => setIsAnimating(false), 300);
    }, 300);

    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [value]);

  return (
    <div className="debounced-animation">
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type something (debounced animation)"
      />
      
      <div className="debounced-display">
        <p>Current: {value}</p>
        <p>Debounced: {debouncedValue}</p>
      </div>
      
      <div className={`animated-result ${isAnimating ? 'show' : ''}`}>
        {debouncedValue}
      </div>
    </div>
  );
};

// 2. 虚拟化长列表动画
const VirtualizedAnimatedList = () => {
  const [items] = useState(() => 
    Array.from({ length: 1000 }, (_, i) => ({
      id: i,
      text: `Item ${i + 1}`,
      height: 50 + Math.random() * 50
    }))
  );

  return (
    <section className="demo-section">
      <h3>Virtualized Animated List</h3>
      
      <div className="virtual-list-container" style={{ height: '400px' }}>
        {items.map((item, index) => (
          <div
            key={item.id}
            className="virtual-list-item"
            style={{
              height: `${item.height}px`,
              opacity: 1,
              transform: 'translateY(0)',
              willChange: 'transform, opacity'
            }}
          >
            {item.text}
          </div>
        ))}
      </div>
      
      <div className="virtualization-tips">
        <h4>Virtualization Benefits:</h4>
        <ul>
          <li>Only render visible items</li>
          <li>Use Intersection Observer for dynamic rendering</li>
          <li>Recycle DOM elements</li>
          <li>Use CSS containment for performance</li>
        </ul>
      </div>
    </section>
  );
};

通过这些动画技术和性能优化策略,你可以创建流畅、高性能的用户界面动画,同时保持良好的性能表现。关键是选择合适的动画方式,并充分利用现代浏览器的硬件加速能力。

posted @ 2025-11-29 18:18  seven3306  阅读(1)  评论(0)    收藏  举报