fix(core): some dnd perf issues (#9661)

1. page list item are bound two draggables. adding `draggable` prop to WorkbenchLink to mitigate the issue.
2. DndService may not resolve datatransfer when dragging.
This commit is contained in:
pengx17
2025-01-15 15:04:01 +00:00
parent b1896746f9
commit 0ed9258f51
5 changed files with 89 additions and 73 deletions

View File

@@ -87,10 +87,16 @@ export const useDraggable = <D extends DNDData = DNDData>(
}, [...deps, context.toExternalData]); }, [...deps, context.toExternalData]);
useEffect(() => { useEffect(() => {
if (!dragRef.current) { if (
!dragRef.current ||
(typeof options.canDrag === 'boolean' && !options.canDrag)
) {
return; return;
} }
const element = dragRef.current;
const dragHandle = dragHandleRef.current;
const windowEvent = { const windowEvent = {
dragleave: () => { dragleave: () => {
setDraggingPosition(state => setDraggingPosition(state =>
@@ -104,11 +110,11 @@ export const useDraggable = <D extends DNDData = DNDData>(
}, },
}; };
dragRef.current.dataset.affineDraggable = 'true'; element.dataset.affineDraggable = 'true';
const cleanupDraggable = draggable({ const cleanupDraggable = draggable({
element: dragRef.current, element,
dragHandle: dragHandleRef.current ?? undefined, dragHandle: dragHandle ?? undefined,
canDrag: draggableGet(options.canDrag), canDrag: draggableGet(options.canDrag),
getInitialData: draggableGet(options.data), getInitialData: draggableGet(options.data),
getInitialDataForExternal: draggableGet(options.toExternalData), getInitialDataForExternal: draggableGet(options.toExternalData),
@@ -130,8 +136,8 @@ export const useDraggable = <D extends DNDData = DNDData>(
if (enableDropTarget.current) { if (enableDropTarget.current) {
setDropTarget([]); setDropTarget([]);
} }
if (dragRef.current) { if (element) {
dragRef.current.dataset['dragging'] = 'true'; element.dataset['dragging'] = 'true';
} }
options.onDragStart?.(args); options.onDragStart?.(args);
}, },
@@ -153,8 +159,8 @@ export const useDraggable = <D extends DNDData = DNDData>(
if (enableDropTarget.current) { if (enableDropTarget.current) {
setDropTarget([]); setDropTarget([]);
} }
if (dragRef.current) { if (element) {
delete dragRef.current.dataset['dragging']; delete element.dataset['dragging'];
} }
options.onDrop?.(args); options.onDrop?.(args);
}, },

View File

@@ -1,3 +1,4 @@
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter'; import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
import type { import type {
@@ -17,7 +18,14 @@ import {
type Instruction, type Instruction,
type ItemMode, type ItemMode,
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { shallowUpdater } from '../../utils'; import { shallowUpdater } from '../../utils';
import { getAdaptedEventArgs, isExternalDrag } from './common'; import { getAdaptedEventArgs, isExternalDrag } from './common';
@@ -201,9 +209,16 @@ export const useDropTarget = <D extends DNDData = DNDData>(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, dropTargetContext.fromExternalData]); }, [...deps, dropTargetContext.fromExternalData]);
const dropTargetOptions = useMemo(() => { const getDropTargetOptions = useCallback(() => {
const wrappedCanDrop = dropTargetGet(options.canDrop, options); const wrappedCanDrop = dropTargetGet(options.canDrop, options);
let _element: HTMLElement | null = null; let element: HTMLElement | null = dropTargetRef.current;
if (
!element ||
(typeof options.canDrop === 'boolean' && !options.canDrop)
) {
return null;
}
const updateDragOver = ( const updateDragOver = (
args: DropTargetDragEvent<D>, args: DropTargetDragEvent<D>,
@@ -254,12 +269,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
}; };
return { return {
get element() { element,
if (!_element) {
_element = dropTargetRef.current;
}
return _element;
},
canDrop: wrappedCanDrop canDrop: wrappedCanDrop
? (args: DropTargetGetFeedback<D>) => { ? (args: DropTargetGetFeedback<D>) => {
// check if args has data. if not, it's an external drag // check if args has data. if not, it's an external drag
@@ -290,8 +300,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
} }
if (options.treeInstruction) { if (options.treeInstruction) {
setTreeInstruction(null); setTreeInstruction(null);
if (dropTargetRef.current) { if (element) {
delete dropTargetRef.current.dataset['treeInstruction']; delete element.dataset['treeInstruction'];
} }
} }
if (options.closestEdge) { if (options.closestEdge) {
@@ -300,8 +310,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
if (enableDropEffect.current) { if (enableDropEffect.current) {
setDropEffect(null); setDropEffect(null);
} }
if (dropTargetRef.current) { if (element) {
delete dropTargetRef.current.dataset['draggedOver']; delete element.dataset['draggedOver'];
} }
// external data is only available in drop event thus // external data is only available in drop event thus
@@ -318,10 +328,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
return; return;
} }
if ( if (args.location.current.dropTargets[0]?.element === element) {
args.location.current.dropTargets[0]?.element ===
dropTargetRef.current
) {
options.onDrop?.({ options.onDrop?.({
...args, ...args,
treeInstruction: extractInstruction(args.self.data), treeInstruction: extractInstruction(args.self.data),
@@ -376,19 +383,15 @@ export const useDropTarget = <D extends DNDData = DNDData>(
}, },
onDropTargetChange: (args: DropTargetDropEvent<D>) => { onDropTargetChange: (args: DropTargetDropEvent<D>) => {
args = getAdaptedEventArgs(args, options.fromExternalData); args = getAdaptedEventArgs(args, options.fromExternalData);
if ( if (args.location.current.dropTargets[0]?.element === element) {
args.location.current.dropTargets[0]?.element ===
dropTargetRef.current
) {
if (enableDraggedOver.current) { if (enableDraggedOver.current) {
setDraggedOver(true); setDraggedOver(true);
} }
if (options.treeInstruction) { if (options.treeInstruction) {
const instruction = extractInstruction(args.self.data); const instruction = extractInstruction(args.self.data);
setTreeInstruction(instruction); setTreeInstruction(instruction);
if (dropTargetRef.current) { if (element) {
dropTargetRef.current.dataset['treeInstruction'] = element.dataset['treeInstruction'] = instruction?.type;
instruction?.type;
} }
} }
if (options.closestEdge) { if (options.closestEdge) {
@@ -410,8 +413,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
clientY: args.location.current.input.clientY, clientY: args.location.current.input.clientY,
}); });
} }
if (dropTargetRef.current) { if (element) {
dropTargetRef.current.dataset['draggedOver'] = 'true'; element.dataset['draggedOver'] = 'true';
} }
} else { } else {
if (enableDraggedOver.current) { if (enableDraggedOver.current) {
@@ -422,8 +425,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
} }
if (options.treeInstruction) { if (options.treeInstruction) {
setTreeInstruction(null); setTreeInstruction(null);
if (dropTargetRef.current) { if (element) {
delete dropTargetRef.current.dataset['treeInstruction']; delete element.dataset['treeInstruction'];
} }
} }
if (enableDropEffect.current) { if (enableDropEffect.current) {
@@ -440,8 +443,8 @@ export const useDropTarget = <D extends DNDData = DNDData>(
if (options.closestEdge) { if (options.closestEdge) {
setClosestEdge(null); setClosestEdge(null);
} }
if (dropTargetRef.current) { if (element) {
delete dropTargetRef.current.dataset['draggedOver']; delete element.dataset['draggedOver'];
} }
} }
}, },
@@ -449,18 +452,26 @@ export const useDropTarget = <D extends DNDData = DNDData>(
}, [options]); }, [options]);
useEffect(() => { useEffect(() => {
if (!dropTargetRef.current) { const dropTargetOptions = getDropTargetOptions();
if (!dropTargetOptions) {
return; return;
} }
return dropTargetForElements(dropTargetOptions as any);
}, [dropTargetOptions]);
useEffect(() => { // @ts-expect-error: fix type error
if (!dropTargetRef.current || !options.fromExternalData) { const cleanup = [dropTargetForElements(dropTargetOptions)];
return;
if (options.allowExternal && options.fromExternalData) {
// @ts-expect-error: fix type error
cleanup.push(dropTargetForExternal(dropTargetOptions));
} }
return dropTargetForExternal(dropTargetOptions as any);
}, [dropTargetOptions, options.fromExternalData]); return combine(...cleanup);
}, [
getDropTargetOptions,
options.canDrop,
options.allowExternal,
options.fromExternalData,
]);
return { return {
dropTargetRef, dropTargetRef,

View File

@@ -382,7 +382,7 @@ const PageListItemWrapper = forwardRef(
if (to) { if (to) {
return ( return (
<WorkbenchLink ref={ref} {...commonProps} to={to}> <WorkbenchLink ref={ref} draggable={false} {...commonProps} to={to}>
{children} {children}
</WorkbenchLink> </WorkbenchLink>
); );

View File

@@ -81,7 +81,7 @@ export class DndService extends Service {
isDropEvent?: boolean isDropEvent?: boolean
) => { ) => {
if (!isDropEvent) { if (!isDropEvent) {
return this.resolveBlocksuiteExternalData(args.source) || {}; return {};
} }
let resolved: AffineDNDData['draggable'] | null = null; let resolved: AffineDNDData['draggable'] | null = null;
@@ -168,30 +168,24 @@ export class DndService extends Service {
if (!dndAPI) { if (!dndAPI) {
return null; return null;
} }
const encoded = source.getStringData(dndAPI.mimeType);
if (source.types.includes(dndAPI.mimeType)) { if (!encoded) {
const from = { return null;
at: 'blocksuite-editor',
} as const;
let entity: Entity | null = null;
const encoded = source.getStringData(dndAPI.mimeType);
const snapshot = encoded ? dndAPI.decodeSnapshot(encoded) : null;
entity = snapshot ? this.resolveBlockSnapshot(snapshot) : null;
if (!entity) {
return {
from,
};
} else {
return {
entity,
from,
};
}
} }
return null; const snapshot = dndAPI.decodeSnapshot(encoded);
if (!snapshot) {
return null;
}
const entity = this.resolveBlockSnapshot(snapshot);
if (!entity) {
return null;
}
return {
entity,
from: {
at: 'blocksuite-editor',
},
};
}; };
private readonly resolveHTML: EntityResolver = html => { private readonly resolveHTML: EntityResolver = html => {

View File

@@ -47,7 +47,10 @@ function resolveToEntity(
} }
export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>( export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
function WorkbenchLink({ to, onClick, replaceHistory, ...other }, ref) { function WorkbenchLink(
{ to, onClick, draggable = true, replaceHistory, ...other },
ref
) {
const { workbenchService } = useServices({ const { workbenchService } = useServices({
WorkbenchService, WorkbenchService,
}); });
@@ -79,8 +82,10 @@ export const WorkbenchLink = forwardRef<HTMLAnchorElement, WorkbenchLinkProps>(
to: stringTo, to: stringTo,
}, },
}, },
canDrag:
typeof draggable === 'boolean' ? draggable : draggable === 'true',
}; };
}, [to, basename, stringTo]); }, [to, basename, stringTo, draggable]);
return ( return (
<a <a