chore: optimize common logic

This commit is contained in:
zhouxiao.shaw 2025-05-26 17:15:57 +08:00
parent 7cc296031b
commit c82d0c493d
3 changed files with 82 additions and 125 deletions

View File

@ -32,30 +32,26 @@ interface FormData {
const App: React.FC = () => {
const [form] = Form.useForm();
const [transformedEvents, setTransformedEvents] = useState<RecordedEvent[]>([]);
const [optimizedEvents, setOptimizedEvents] = useState<RecordedEvent[]>([]);
const [rawEventsCount, setRawEventsCount] = useState(0);
const [mergedEventsCount, setMergedEventsCount] = useState(0);
const eventRecorderRef = useRef<EventRecorder | null>(null);
const eventOptimizerRef = useRef<EventOptimizer | null>(null);
useEffect(() => {
// 创建事件优化器
eventOptimizerRef.current = new EventOptimizer();
// 创建事件记录器
eventRecorderRef.current = new EventRecorder((event: RecordedEvent) => {
setRawEventsCount((prev) => prev + 1);
if (eventOptimizerRef.current) {
const optimizedEvents = eventOptimizerRef.current.addEvent(event);
setTransformedEvents(optimizedEvents);
console.log('All Events:', optimizedEvents);
const optimized = eventOptimizerRef.current.addEvent(event);
console.log('optimized', optimized)
setOptimizedEvents([...optimized]);
setMergedEventsCount(eventOptimizerRef.current.getEventCount());
}
});
// 开始记录
eventRecorderRef.current.start();
return () => {
// 停止记录
if (eventRecorderRef.current) {
eventRecorderRef.current.stop();
}
@ -67,10 +63,8 @@ const App: React.FC = () => {
message.error('两次输入的密码不一致!');
return;
}
console.log('Form Data:', values);
console.log('Recorded Events:', transformedEvents);
console.log('Optimized Events:', optimizedEvents);
message.success('注册成功!');
};
@ -97,7 +91,6 @@ const App: React.FC = () => {
>
<Form.Item
label="用户名"
htmlFor="null"
name="username"
rules={[
{ required: true, message: '请输入用户名!' },
@ -207,10 +200,10 @@ const App: React.FC = () => {
<div className="rr-ignore" style={{ marginTop: 20, fontSize: 12, color: '#666', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<p style={{ margin: '4px 0' }}>📊 </p>
<p style={{ margin: '4px 0' }}>: {transformedEvents.length}</p>
<p style={{ margin: '4px 0' }}>🔄 : {mergedEventsCount}</p>
<p style={{ margin: '4px 0' }}>: {rawEventsCount}</p>
<p style={{ margin: '4px 0' }}>: {mergedEventsCount}</p>
<p style={{ margin: '4px 0', fontSize: '10px', color: '#999' }}>
: {transformedEvents.length > 0 ? Math.round((mergedEventsCount / transformedEvents.length) * 100) : 0}%
: {rawEventsCount > 0 ? Math.round((1 - mergedEventsCount / rawEventsCount) * 100) : 0}%
</p>
</div>
</Card>

View File

@ -12,7 +12,7 @@ export class EventOptimizer {
return [...this.events];
}
// 如果是输入事件,检查是否需要跳过
// 如果是输入事件,检查是否需要跳过或合并
if (event.type === 'input') {
const shouldSkip = this.shouldSkipInputEvent(event);
if (shouldSkip) {
@ -23,6 +23,24 @@ export class EventOptimizer {
});
return [...this.events];
}
const shouldMerge = this.shouldMergeInputEvent(event);
if (shouldMerge) {
// 获取旧的输入事件信息用于日志
const oldInputEvent = this.events[this.events.length - 1];
// 用新的输入事件替换最后一个输入事件
this.events[this.events.length - 1] = {
value: (event.element as HTMLInputElement)?.value,
...event,
};
console.log('Merging input event:', {
oldValue: oldInputEvent.value,
newValue: event.value,
oldTimestamp: oldInputEvent.timestamp,
newTimestamp: event.timestamp,
target: event.targetTagName,
});
return [...this.events];
}
}
// 如果是滚动事件,检查是否需要替换上一个滚动事件
@ -31,10 +49,8 @@ export class EventOptimizer {
if (shouldReplace) {
// 获取旧的滚动事件信息用于日志
const oldScrollEvent = this.events[this.events.length - 1];
// 用新的滚动事件替换最后一个滚动事件
this.events[this.events.length - 1] = event;
console.log('Replacing last scroll event with new scroll event:', {
oldPosition: `${oldScrollEvent.x},${oldScrollEvent.y}`,
newPosition: `${event.x},${event.y}`,
@ -68,6 +84,45 @@ export class EventOptimizer {
return false;
}
// 检查是否应该合并输入事件
private shouldMergeInputEvent(inputEvent: RecordedEvent): boolean {
const lastEvent = this.getLastEvent();
// 如果上一个事件是输入事件,并且是同一个输入目标
if (
lastEvent &&
lastEvent.type === 'input' &&
this.isSameInputTarget(lastEvent, inputEvent)
) {
return true;
}
return false;
}
// 检查是否是同一个输入目标
private isSameInputTarget(
event1: RecordedEvent,
event2: RecordedEvent,
): boolean {
// 比较元素标签名和ID
if (event1.targetTagName !== event2.targetTagName) {
return false;
}
// 如果都有ID比较ID
if (event1.targetId && event2.targetId) {
return event1.targetId === event2.targetId;
}
// 如果都没有ID比较标签名通常是document或body
if (!event1.targetId && !event2.targetId) {
return event1.targetTagName === event2.targetTagName;
}
return false;
}
// 检查是否应该替换滚动事件
private shouldReplaceScrollEvent(scrollEvent: RecordedEvent): boolean {
const lastEvent = this.getLastEvent();

View File

@ -30,9 +30,7 @@ export class EventRecorder {
private isRecording = false;
private eventCallback: EventCallback;
private scrollThrottleTimer: number | null = null;
private lastScrollEvent: RecordedEvent | null = null;
private scrollThrottleDelay = 1000; // 1000ms 节流
private events: RecordedEvent[] = [];
private lastViewportScroll: { x: number; y: number } | null = null;
constructor(eventCallback: EventCallback) {
@ -46,9 +44,9 @@ export class EventRecorder {
this.isRecording = true;
// 添加事件监听器
document.addEventListener('click', this.handleClick, true);
document.addEventListener('scroll', this.handleScroll, true);
document.addEventListener('input', this.handleInput, true);
document.addEventListener('click', this.handleClick);
document.addEventListener('scroll', this.handleScroll);
document.addEventListener('input', this.handleInput);
// 添加页面加载事件
const navigationEvent: RecordedEvent = {
@ -72,9 +70,9 @@ export class EventRecorder {
}
// 移除事件监听器
document.removeEventListener('click', this.handleClick, true);
document.removeEventListener('scroll', this.handleScroll, true);
document.removeEventListener('input', this.handleInput, true);
document.removeEventListener('click', this.handleClick);
document.removeEventListener('scroll', this.handleScroll);
document.removeEventListener('input', this.handleInput);
}
// 点击事件处理器
@ -83,25 +81,11 @@ export class EventRecorder {
const target = event.target as HTMLElement;
// 优化:如果上一个事件是 label 点击,并且 labelInfo.htmlFor 等于当前 input 的 id则跳过
const lastEvent = this.getLastEvent();
if (
lastEvent &&
lastEvent.type === 'click' &&
lastEvent.isLabelClick &&
lastEvent.labelInfo?.htmlFor === target.id
) {
console.log('Skip input event triggered by label click:', target.id);
return;
}
// 检查是否是 label 触发的点击
const { isLabelClick, labelInfo } = this.checkLabelClick(target);
// 获取元素相对于 viewport 的位置
const rect = target.getBoundingClientRect();
const relativeX = rect.left;
const relativeY = rect.top;
const clickEvent: RecordedEvent = {
type: 'click',
@ -117,12 +101,10 @@ export class EventRecorder {
targetClassName: target?.className,
isTrusted: event.isTrusted,
detail: event.detail,
viewportX: relativeX,
viewportY: relativeY,
viewportX: rect.left,
viewportY: rect.top,
};
console.log('Click Event:', clickEvent);
this.events.push(clickEvent);
this.eventCallback(clickEvent);
};
@ -151,7 +133,7 @@ export class EventRecorder {
target instanceof Document ? window.scrollY : target.scrollTop;
// 始终保存最新的滚动事件
this.lastScrollEvent = {
const scrollEvent: RecordedEvent = {
type: 'scroll',
x: scrollXTarget,
y: scrollYTarget,
@ -168,18 +150,8 @@ export class EventRecorder {
}
this.scrollThrottleTimer = window.setTimeout(() => {
if (this.lastScrollEvent && this.isRecording) {
console.log('Throttled Scroll Event:', this.lastScrollEvent);
// 优化:如有必要,替换最后一个 scroll 事件,否则 push
if (this.shouldReplaceScrollEvent(this.lastScrollEvent)) {
this.events[this.events.length - 1] = this.lastScrollEvent;
} else {
this.events.push(this.lastScrollEvent);
}
this.eventCallback(this.lastScrollEvent);
this.lastScrollEvent = null;
if (scrollEvent && this.isRecording) {
this.eventCallback(scrollEvent);
}
this.scrollThrottleTimer = null;
}, this.scrollThrottleDelay);
@ -191,22 +163,8 @@ export class EventRecorder {
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
// 优化:如果上一个事件是 label 点击,并且 labelInfo.htmlFor 等于当前 input 的 id则跳过
const lastEvent = this.getLastEvent();
if (
lastEvent &&
lastEvent.type === 'click' &&
lastEvent.isLabelClick &&
lastEvent.labelInfo?.htmlFor === target.id
) {
console.log('Skip input event triggered by label click:', target.id);
return;
}
// 获取元素相对于 viewport 的位置
const rect = target.getBoundingClientRect();
const relativeX = rect.left;
const relativeY = rect.top;
const inputEvent: RecordedEvent = {
type: 'input',
@ -217,12 +175,10 @@ export class EventRecorder {
targetId: target?.id,
targetClassName: target?.className,
inputType: target.type || 'text',
viewportX: relativeX,
viewportY: relativeY,
viewportX: rect.left,
viewportY: rect.top,
};
console.log('Input Event:', inputEvent);
this.events.push(inputEvent);
this.eventCallback(inputEvent);
};
@ -267,51 +223,4 @@ export class EventRecorder {
isActive(): boolean {
return this.isRecording;
}
private getLastEvent(): RecordedEvent | undefined {
return this.events[this.events.length - 1];
}
// 检查是否应该替换滚动事件
private shouldReplaceScrollEvent(scrollEvent: RecordedEvent): boolean {
const lastEvent = this.getLastEvent();
// 如果最后一个事件是滚动事件,并且是同一个元素,则替换
if (
lastEvent &&
lastEvent.type === 'scroll' &&
this.isSameScrollTarget(lastEvent, scrollEvent)
) {
return true;
}
return false;
}
// 检查是否是同一个滚动目标
private isSameScrollTarget(
event1: RecordedEvent,
event2: RecordedEvent,
): boolean {
// 比较元素标签名和ID
if (event1.targetTagName !== event2.targetTagName) {
return false;
}
// 如果都有ID比较ID
if (event1.targetId && event2.targetId) {
return event1.targetId === event2.targetId;
}
// 如果都没有ID比较标签名通常是document或body
if (!event1.targetId && !event2.targetId) {
return event1.targetTagName === event2.targetTagName;
}
return false;
}
getEvents(): RecordedEvent[] {
return [...this.events];
}
}