pragmatic-drag-and-drop 之 react-beautiful-dnd-autoscroll 自动滚动包:API、滚动算法与迁移指南
2026/9/15 17:29:59 网站建设 项目流程

pragmatic-drag-and-drop 之 react-beautiful-dnd-autoscroll 自动滚动包:API、滚动算法与迁移指南

【免费下载链接】pragmatic-drag-and-dropFast drag and drop for any experience on any tech stack项目地址: https://gitcode.com/GitHub_Trending/pr/pragmatic-drag-and-drop

本篇文章围绕 react-beautiful-dnd-autoscroll/README.md 展开,深入解析 Pragmatic drag and drop 生态中这个"可选自动滚动包"的定位、公开 API、内部滚动算法(距离阈值、速度曲线、时间阻尼)以及官方建议的替代方案。读完本文,你将掌握autoScrollercreateAutoScroller的正确用法、四种ScrollBehavior的取舍,以及何时应该迁移到新一代的 auto-scroll 包。

一、包的定位:从 react-beautiful-dnd 移植而来的自动滚动器

react-beautiful-dnd-autoscroll 是 Pragmatic drag and drop 的可选包之一,作用是在拖拽进行过程中自动滚动滚动容器或窗口,让用户把拖拽项靠近容器边缘时无需手动滚动,即可将内容"带"到视野内。README 明确说明,它是 react-beautiful-dnd 自动滚动器的一次移植(port),因此其算法、常量与实现风格都带有 rbd 的印记(源码多处标注// Source: https://github.com/atlassian/react-beautiful-dnd)。

需要注意,README 在开头即给出了重要警告:

⚠️ 我们不推荐使用该包,请使用新的 auto scroller 包(即 packages/auto-scroll),并计划弃用本包。

也就是说,对于新项目,官方推荐直接使用 auto-scroll 新包;本包更适合已有 rbd 迁移背景、需要与旧代码行为对齐的场景。README 中还给出了文档链接指向 atlassian.design 的官方文档页,但仓库本身的内容(源码与测试)是我们本文最可靠的事实来源。

从 package.json 可以看到该包的元信息:

  • 包名:@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll(当前仓库中版本为 3.1.0)
  • 许可证:Apache-2.0
  • 依赖:@atlaskit/pragmatic-drag-and-drop(核心包,^3.0.0)、css-box-model(用于矩形几何计算)、@babel/runtime
  • 导出映射:根路径导出全部 API,同时提供./create-auto-scroller./auto-scroller两个子路径导出

二、快速上手:使用 autoScroller 单例接入拖拽生命周期

包在 src/index.ts 中只导出两个东西:createAutoScroller(工厂函数)和autoScroller(预先创建好的单例实例)。

单例autoScroller在 src/autoScroller.tsx 中定义,它就是调用一次createAutoScroller()的结果,接口为:

export const autoScroller: { start: ({ input, behavior }: { input: Input; behavior?: ScrollBehavior }) => void; updateInput: ({ input }: { input: Input }) => void; stop: () => void; } = createAutoScroller();

接入方式非常直观:在拖拽开始时调用start,在拖拽过程中不断用最新指针位置调用updateInput,在拖拽结束时调用stop。一个基于核心包 drag 事件的典型用法如下:

import { autoScroller } from '@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll'; draggable({ element, onDragStart: ({ input }) => { autoScroller.start({ input }); }, onDrag: ({ input }) => { // 持续用最新指针位置更新内部状态 autoScroller.updateInput({ input }); }, onDrop: () => { autoScroller.stop(); }, });

其中Input是核心包暴露的类型,包含clientXclientY等指针坐标信息。behavior是可选参数,用于控制"先滚窗口还是先滚容器",默认值为'window-then-container',详见下文第四节。

单例 vs 工厂

如果应用中有多个拖拽场景且需要各自独立的滚动状态,可以使用工厂函数 createAutoScroller 创建互不干扰的实例:

import { createAutoScroller } from '@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll'; const myScroller = createAutoScroller(); myScroller.start({ input }); myScroller.updateInput({ input }); myScroller.stop();

三、底层机制:rAF 驱动循环与三个公开方法

createAutoScroller.tsx 的实现揭示了自动滚动的核心工作机制。它内部维护一个dragging状态对象,记录dragStartTimelatestInput(最新输入)、loopFrameId(动画帧句柄)、shouldUseTimeDampening(是否启用时间阻尼)以及behavior

1.start:记录起始时间并启动循环

const start = ({ input, behavior = 'window-then-container' }): void => { const dragStartTime: number = Date.now(); dragging = { dragStartTime, latestInput: input, loopFrameId: null, shouldUseTimeDampening: false, behavior }; const fakeScrollCallback = () => { if (dragging) dragging.shouldUseTimeDampening = true; }; tryScroll(fakeScrollCallback); loop(); };

这里有一个巧妙的细节:start首次调用tryScroll时传入的是一个fakeScrollCallback而非真实滚动函数——只要首次尝试判定"需要滚动",就会把shouldUseTimeDampening置为true。源码注释解释得很清楚:时间阻尼只在拖拽刚刚抬起(lift)那一刻就发生自动滚动时启用,这样可以在拖拽一开始避免突然的"猛冲"感。后续的循环则使用真实的scrollElement/scrollWindow

2.updateInput:只更新、不触发

function updateInput({ input }): void { if (!dragging) return; dragging.latestInput = input; }

拖拽过程中onDrag事件会频繁触发,但自动滚动器并不依赖事件频率——它只把最新坐标存入latestInput,真正的滚动动作由 rAF 循环读取。

3.stop:防御性地取消动画帧

const stop = (): void => { if (!dragging) return; // 可以防御性地调用 if (dragging.loopFrameId) cancelAnimationFrame(dragging.loopFrameId); dragging = null; };

rAF 循环:为什么不用 onDrag 事件驱动?

loop使用requestAnimationFrame自循环:

function loop() { if (!dragging) return; dragging.loopFrameId = requestAnimationFrame(() => { tryScroll(); loop(); }); }

源码注释给出了明确理由:用户不主动移动指针时,onDrag事件之间可能间隔 50–100ms(浏览器事件节流),如果只在事件回调里滚动,滚动会变得卡顿;而 rAF 按屏幕刷新率(通常 60fps)驱动,能保证即使指针静止在容器边缘,滚动也持续平滑进行。

四、四种滚动行为(ScrollBehavior)详解

ScrollBehavior类型定义在 src/internal/types.ts:

export type ScrollBehavior = | 'window-then-container' // 默认:先尝试滚窗口,不行再滚容器 | 'container-then-window' // 先尝试滚容器,不行再滚窗口 | 'window-only' // 只滚窗口 | 'container-only'; // 只滚容器

scroll.ts 中实现了四种行为的调度逻辑:

if (behavior === 'container-only') tryScrollContainer(); if (behavior === 'window-only') tryScrollWindow(); if (behavior === 'container-then-window') tryScrollContainer() || tryScrollWindow(); if (behavior === 'window-then-container') tryScrollWindow() || tryScrollContainer();

其中tryScrollWindow会读取当前 viewport(窗口容器矩形与滚动位置),计算基于指针中心的窗口滚动量;tryScrollContainer则通过getElementFromPointWithoutHoneypot获取指针正下方的元素,再向上查找最近的可滚动祖先(getClosestScrollableElement),并计算该容器的滚动量。注意这里复用了核心包的 honey-pot 修复能力,避免被核心包插入的"蜜罐"元素干扰元素拾取。

五、滚动算法原理:距离阈值、速度曲线与时间阻尼

自动滚动的"手感"由三个层次共同决定,全部集中在 config.ts:

const config = { startFromPercentage: 0.25, // 距离边缘 25% 处开始触发滚动 maxScrollAtPercentage: 0.05, // 距离边缘 5% 以内达到最大速度 maxPixelScroll: 28, // 每帧最大滚动像素数 ease: (percentage) => Math.pow(percentage, 2), // 二次缓动 durationDampening: { stopDampeningAt: 1200, // 1200ms 后停止时间阻尼 accelerateAt: 360, // 360ms 时开始加速 }, };

1. 距离阈值:百分比 → 像素

get-distance-thresholds.ts 把配置中的百分比换算成实际像素:

const startScrollingFrom = container[axis.size] * config.startFromPercentage; const maxScrollValueAt = container[axis.size] * config.maxScrollAtPercentage;

例如一个高 400px 的容器,指针距上/下边缘 100px(25%)以内开始滚动,距边缘 20px(5%)以内达到最大速度。

2. 距离 → 速度

get-value-from-distance.ts 实现"越近越快"的速度曲线:

  • 距离超过startScrollingFrom:返回 0(不滚动);
  • 距离小于等于maxScrollValueAt:直接返回maxPixelScroll(28px/帧,封顶);
  • 其余区间:用getPercentage计算当前位置在两个阈值间的比例,取反后再经ease(平方)缓动,最后Math.ceil向上取整保证产生整数像素。

即滚动速度从边缘往内呈二次曲线衰减:靠近边缘时快速逼近 28px/帧,远离边缘时平缓降到 1px/帧(minScroll常量定义在 constants.ts,为 1px——scrollBy只有在位移 ≥1px 时才会真正触发滚动事件)。

3. 时间阻尼:拖拽开始瞬间的"软启动"

dampen-value-by-time.ts 实现时间阻尼:

  • 拖拽运行时间 <accelerateAt(360ms):只允许最小滚动量 1px;
  • 运行时间 ≥stopDampeningAt(1200ms):完全解除阻尼,返回原始速度;
  • 两者之间:按getPercentage插值并再次经过ease缓动放大。

叠加逻辑在 get-value.ts 中:先按距离算出原始速度,若为 0 直接返回;若启用了时间阻尼,则取max(dampenValueByTime(scroll), minScroll)——至少放行 1px,保证滚动事件链条不中断(源码注释明确说明这是为了让滚动事件持续触发、进而维持自动滚动循环)。

4. 双轴计算与"能否滚动"校验

get-scroll/index.ts 同时计算纵轴(vertical)与横轴(horizontal)两个方向的滚动量:先计算指针到容器四边的距离,对每个轴判断"离起点近还是离终点近"从而确定正负方向,最后如果合成位移为{0, 0}则返回null表示无需滚动。窗口场景由 get-window-scroll-change.ts 额外做一步canScrollWindow校验(容器或窗口已到滚动边界时不再产生滚动量),容器场景同样有canScrollScrollable等边界检查,相关源码位于 src/internal 目录下。

六、测试验证:算法正确性的佐证

该包在tests/unit 下提供了完整的单元测试,覆盖了上面提到的各个算法环节,例如:

  • auto-scrolling.spec.ts:端到端验证自动滚动整体行为;
  • get-closest-scrollable.spec.ts:验证从指针位置元素向上查找最近可滚动祖先;
  • get-max-scroll.spec.tsget-percentage.spec.ts:验证滚动上限与百分比插值计算;
  • get-scroll/子目录下的 6 个测试文件:分别验证距离阈值、速度取值、时间阻尼等核心函数。

阅读这些测试可以快速理解算法的边界行为(例如"距离刚好等于startScrollingFrom时返回最小滚动量 1px"、"距离进入maxScrollValueAt区间时直接命中最大速度"等),是深入理解本包行为的最佳入口。

七、迁移建议:从本包迁移到 auto-scroll 新包

README 的弃用警告明确指向新包 auto-scroll,其目录结构(src/over-elementsrc/unsafe-overflowsrc/sharedsrc/entry-point)表明新包在架构上做了大幅演进:滚动逻辑被拆分为 "over element"(指针悬浮于元素上触发滚动)与 "unsafe overflow"(溢出容器边缘触发滚动)两套模型,并在constellation/index/about.mdx(auto-scroll 说明文档)中描述了设计理念,同时提供了更细化的配置(距离阻尼、速度上限、时间阻尼等参数在config.ts中分别暴露)。新包还内置了tryScrollmakeApi等 API 工厂与完整的 Playwright 冒烟测试。

迁移时建议关注三点

  1. API 形态变化:新包不再是start/updateInput/stop的全局单例模式,而是通过autoScroller({ input, element, behavior })等按元素/场景组织的 API(见 auto-scroll 入口),接入方式需要相应改写;
  2. 滚动模型差异:新包的 over-element 模型在计算 hitbox 时引入了允许轴(allowed axis)、距离阻尼与时间阻尼的独立组合(参见 config.ts),手感与本包并不完全一致,迁移后建议重新做一遍手感验收;
  3. 弃用节奏:README 只是声明"计划弃用",并未给出具体时间表,因此存量代码可以继续使用本包,但新功能开发应优先落在新包上。

八、总结

react-beautiful-dnd-autoscroll 是一个小而精的自动滚动包:它用 rAF 循环保证滚动平滑、用二次缓动曲线控制"越近越快"的手感、用时间阻尼解决拖拽瞬间的突兀加速,并通过四种ScrollBehavior灵活调度窗口与容器的滚动优先级。虽然官方已建议新项目改用 auto-scroll 新包,但理解本包的算法(距离阈值换算、速度取值、时间阻尼三段式)依然是掌握 Pragmatic drag and drop 自动滚动设计思想的最佳切入点——新包的核心参数(startFromPercentagemaxScrollAtPercentagemaxPixelScrolldurationDampening等)在本包中都能找到同源的对应物。

【免费下载链接】pragmatic-drag-and-dropFast drag and drop for any experience on any tech stack项目地址: https://gitcode.com/GitHub_Trending/pr/pragmatic-drag-and-drop

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询