React 版本:18.x 难度:中级
考察要点:
- createPortal 的使用
- 模态框生命周期管理
- 事件冒泡处理
- 可访问性考虑
解答:
1. 概念解释
基本定义:
- Portal:将子节点渲染到父组件 DOM 层次之外的机制
- 模态框:覆盖在应用程序主窗口上的对话框
- 事件传播:保持 React 事件系统的完整性
工作原理:
- DOM 节点的跨层级渲染
- 事件冒泡遵循 React 树而非 DOM 树
- 保持上下文和状态的传递
应用场景:
- 模态对话框
- 工具提示
- 悬浮卡片
- 全局通知
2. 代码示例
基础示例:
import React, { useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
// 👉 基础模态框组件
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children
}) => {
// ESC 键关闭处理
const handleEscapeKey = useCallback((event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
}, [onClose]);
// 添加/移除事件监听器
useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleEscapeKey);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscapeKey);
document.body.style.overflow = 'unset';
};
}, [isOpen, handleEscapeKey]);
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button
onClick={onClose}
aria-label="Close modal"
className="close-button"
>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
document.body
);
};
进阶示例:
import React, { useState, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
// 👉 模态框上下文类型
interface ModalContextType {
openModal: (content: React.ReactNode) => void;
closeModal: () => void;
}
// 👉 创建上下文
const ModalContext = React.createContext<ModalContextType | undefined>(undefined);
// 👉 模态框提供者组件
export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const [modalContent, setModalContent] = useState<React.ReactNode | null>(null);
const [isOpen, setIsOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const openModal = useCallback((content: React.ReactNode) => {
setModalContent(content);
setIsOpen(true);
}, []);
const closeModal = useCallback(() => {
setIsOpen(false);
setTimeout(() => setModalContent(null), 300); // 动画结束后清除内容
}, []);
// 处理动画
const getAnimationClass = () => {
if (!isOpen && modalContent) return 'modal-exit';
if (isOpen) return 'modal-enter';
return '';
};
return (
<ModalContext.Provider value={{ openModal, closeModal }}>
{children}
{modalContent && createPortal(
<div
className={`modal-container ${getAnimationClass()}`}
ref={modalRef}
role="dialog"
aria-modal="true"
>
<div className="modal-backdrop" onClick={closeModal} />
<div className="modal-window">
<div className="modal-content">
{modalContent}
</div>
</div>
</div>,
document.body
)}
</ModalContext.Provider>
);
};
// 👉 自定义 Hook
export const useModal = () => {
const context = React.useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used within a ModalProvider');
}
return context;
};
// 👉 使用示例
const ModalExample: React.FC = () => {
const { openModal } = useModal();
const handleOpenModal = () => {
openModal(
<div>
<h2>Custom Modal Content</h2>
<p>This is a dynamic modal content example.</p>
</div>
);
};
return (
<button onClick={handleOpenModal}>
Open Modal
</button>
);
};
3. 注意事项与最佳实践
❌ 常见错误示例:
// ❌ 错误示范:不处理事件冒泡
const BadModal: React.FC = () => {
return createPortal(
<div className="modal">
<div className="content">
{/* 点击内容区域也会触发关闭 */}
Content
</div>
</div>,
document.body
);
};
// ❌ 错误示范:忽略可访问性
const AnotherBadModal: React.FC = () => {
return createPortal(
<div>
{/* 缺少适当的 ARIA 属性 */}
<div>Modal content</div>
</div>,
document.body
);
};
✅ 正确实现方式:
// ✅ 正确示范:完整的模态框实现
const GoodModal: React.FC<{
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}> = ({ isOpen, onClose, children }) => {
// 处理点击事件
const handleContentClick = (e: React.MouseEvent) => {
e.stopPropagation();
};
return createPortal(
<div
className={`modal-overlay ${isOpen ? 'open' : ''}`}
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
className="modal-content"
onClick={handleContentClick}
>
<div className="modal-header">
<button
onClick={onClose}
aria-label="Close modal"
>
Close
</button>
</div>
{children}
</div>
</div>,
document.body
);
};
// ✅ 正确示范:处理焦点管理
const AccessibleModal: React.FC = () => {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const content = contentRef.current;
if (content) {
// 保存之前的焦点
const previousFocus = document.activeElement;
content.focus();
return () => {
// 恢复焦点
if (previousFocus instanceof HTMLElement) {
previousFocus.focus();
}
};
}
}, []);
return createPortal(
<div
ref={contentRef}
tabIndex={-1}
{...props}
/>,
document.body
);
};
4. 性能优化
import React, { lazy, Suspense } from 'react';
// 👉 懒加载模态框内容
const LazyModalContent = lazy(() =>
import('./ModalContent')
);
// 👉 优化的模态框组件
const OptimizedModal: React.FC<{
isOpen: boolean;
onClose: () => void;
}> = ({ isOpen, onClose }) => {
// 使用 memo 优化子组件
const ModalHeader = React.memo(() => (
<div className="modal-header">
<button onClick={onClose}>Close</button>
</div>
));
return createPortal(
<div className={`modal ${isOpen ? 'open' : ''}`}>
<ModalHeader />
<Suspense fallback={<div>Loading...</div>}>
<LazyModalContent />
</Suspense>
</div>,
document.body
);
};
5. 测试策略
import { render, screen, fireEvent } from '@testing-library/react';
describe('Modal', () => {
it('should render in portal and handle close', () => {
const handleClose = jest.fn();
render(
<Modal isOpen onClose={handleClose} title="Test Modal">
<div>Modal content</div>
</Modal>
);
// 检查模态框内容
expect(screen.getByRole('dialog')).toBeInTheDocument();
// 测试关闭按钮
fireEvent.click(screen.getByLabelText('Close modal'));
expect(handleClose).toHaveBeenCalled();
});
it('should handle escape key', () => {
const handleClose = jest.fn();
render(
<Modal isOpen onClose={handleClose} title="Test Modal">
<div>Modal content</div>
</Modal>
);
// 测试 ESC 键
fireEvent.keyDown(document, { key: 'Escape' });
expect(handleClose).toHaveBeenCalled();
});
});
这个实现展示了 Portal 和模态框的完整使用方案,包括:
- 基础和进阶的实现方式
- 可访问性和事件处理
- 性能优化策略
- 测试方法
关键是要理解 Portal 的工作原理,以及如何正确处理模态框的生命周期、事件传播和可访问性问题。