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

浙公网安备 33010602011771号