浏览代码

feat: throttle `pointermove` events per framerate (#4727)

David Luzar 3 年之前
父节点
当前提交
8aff076782
共有 3 个文件被更改,包括 75 次插入6 次删除
  1. 11 4
      src/components/App.tsx
  2. 2 2
      src/types.ts
  3. 62 0
      src/utils.ts

+ 11 - 4
src/components/App.tsx

@@ -212,6 +212,7 @@ import {
   tupleToCoors,
   viewportCoordsToSceneCoords,
   withBatchedUpdates,
+  withBatchedUpdatesThrottled,
 } from "../utils";
 import ContextMenu, { ContextMenuOption } from "./ContextMenu";
 import LayerUI from "./LayerUI";
@@ -2921,7 +2922,7 @@ class App extends React.Component<AppProps, AppState> {
 
     setCursor(this.canvas, CURSOR_TYPE.GRABBING);
     let { clientX: lastX, clientY: lastY } = event;
-    const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
+    const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
       const deltaX = lastX - event.clientX;
       const deltaY = lastY - event.clientY;
       lastX = event.clientX;
@@ -2984,6 +2985,7 @@ class App extends React.Component<AppProps, AppState> {
         window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
         window.removeEventListener(EVENT.POINTER_UP, teardown);
         window.removeEventListener(EVENT.BLUR, teardown);
+        onPointerMove.flush();
       }),
     );
     window.addEventListener(EVENT.BLUR, teardown);
@@ -3086,7 +3088,7 @@ class App extends React.Component<AppProps, AppState> {
     isDraggingScrollBar = true;
     pointerDownState.lastCoords.x = event.clientX;
     pointerDownState.lastCoords.y = event.clientY;
-    const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
+    const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
       const target = event.target;
       if (!(target instanceof HTMLElement)) {
         return;
@@ -3105,6 +3107,7 @@ class App extends React.Component<AppProps, AppState> {
       this.savePointer(event.clientX, event.clientY, "up");
       window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
       window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
+      onPointerMove.flush();
     });
 
     lastPointerUp = onPointerUp;
@@ -3640,8 +3643,8 @@ class App extends React.Component<AppProps, AppState> {
 
   private onPointerMoveFromPointerDownHandler(
     pointerDownState: PointerDownState,
-  ): (event: PointerEvent) => void {
-    return withBatchedUpdates((event: PointerEvent) => {
+  ) {
+    return withBatchedUpdatesThrottled((event: PointerEvent) => {
       // We need to initialize dragOffsetXY only after we've updated
       // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
       // event handler should hopefully ensure we're already working with
@@ -4062,6 +4065,10 @@ class App extends React.Component<AppProps, AppState> {
 
       lastPointerUp = null;
 
+      if (pointerDownState.eventListeners.onMove) {
+        pointerDownState.eventListeners.onMove.flush();
+      }
+
       window.removeEventListener(
         EVENT.POINTER_MOVE,
         pointerDownState.eventListeners.onMove!,

+ 2 - 2
src/types.ts

@@ -20,7 +20,7 @@ import { LinearElementEditor } from "./element/linearElementEditor";
 import { SuggestedBinding } from "./element/binding";
 import { ImportedDataState } from "./data/types";
 import type App from "./components/App";
-import type { ResolvablePromise } from "./utils";
+import type { ResolvablePromise, throttleRAF } from "./utils";
 import { Spreadsheet } from "./charts";
 import { Language } from "./i18n";
 import { ClipboardData } from "./clipboard";
@@ -367,7 +367,7 @@ export type PointerDownState = Readonly<{
   // We need to have these in the state so that we can unsubscribe them
   eventListeners: {
     // It's defined on the initial pointer down event
-    onMove: null | ((event: PointerEvent) => void);
+    onMove: null | ReturnType<typeof throttleRAF>;
     // It's defined on the initial pointer down event
     onUp: null | ((event: PointerEvent) => void);
     // It's defined on the initial pointer down event

+ 62 - 0
src/utils.ts

@@ -119,6 +119,53 @@ export const debounce = <T extends any[]>(
   return ret;
 };
 
+// throttle callback to execute once per animation frame
+export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
+  let handle: number | null = null;
+  let lastArgs: T | null = null;
+  let callback: ((...args: T) => void) | null = null;
+  const ret = (...args: T) => {
+    if (process.env.NODE_ENV === "test") {
+      fn(...args);
+      return;
+    }
+    lastArgs = args;
+    callback = fn;
+    if (handle === null) {
+      handle = window.requestAnimationFrame(() => {
+        handle = null;
+        lastArgs = null;
+        callback = null;
+        fn(...args);
+      });
+    }
+  };
+  ret.flush = () => {
+    if (handle !== null) {
+      cancelAnimationFrame(handle);
+      handle = null;
+    }
+    if (lastArgs) {
+      const _lastArgs = lastArgs;
+      const _callback = callback;
+      lastArgs = null;
+      callback = null;
+      if (_callback !== null) {
+        _callback(..._lastArgs);
+      }
+    }
+  };
+  ret.cancel = () => {
+    lastArgs = null;
+    callback = null;
+    if (handle !== null) {
+      cancelAnimationFrame(handle);
+      handle = null;
+    }
+  };
+  return ret;
+};
+
 // https://github.com/lodash/lodash/blob/es/chunk.js
 export const chunk = <T extends any>(
   array: readonly T[],
@@ -356,6 +403,21 @@ export const withBatchedUpdates = <
     unstable_batchedUpdates(func as TFunction, event);
   }) as TFunction;
 
+/**
+ * barches React state updates and throttles the calls to a single call per
+ * animation frame
+ */
+export const withBatchedUpdatesThrottled = <
+  TFunction extends ((event: any) => void) | (() => void),
+>(
+  func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
+) => {
+  // @ts-ignore
+  return throttleRAF<Parameters<TFunction>>(((event) => {
+    unstable_batchedUpdates(func, event);
+  }) as TFunction);
+};
+
 //https://stackoverflow.com/a/9462382/8418
 export const nFormatter = (num: number, digits: number): string => {
   const si = [