ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

wp-calypso 中基于 requestAnimationFrame 的平滑滚动工具库 scroll-to 全解析

wp-calypso 中基于 requestAnimationFrame 的平滑滚动工具库 scroll-to 全解析 前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载wp-calypso 客户端中的calypso/lib/scroll-to是一个轻量级的平滑滚动工具模块用于以缓动动画的方式把窗口或任意容器滚动到指定坐标。本文基于模块的 README 文档 与 完整源码 展开先给出完整 API 与可复制用法再逐段拆解其requestAnimationFrame时间步进、缓动函数、容器兼容与「同容器滚动抢占」四大核心机制并结合单元测试与真实调用场景说明其工程取舍适合需要在 wp-calypso 前端做滚动交互的开发者阅读与复用。一、模块定位与基本用法README 对该模块的定义很简洁一个用于平滑滚动到窗口指定位置的实用工具模块A utility module to smoothly scroll to a window position。它的默认导出是一个scrollTo(options)函数接收目标坐标、时长、缓动函数与回调调用后立即返回一个 Stepper 步进器对象滚动动画通过浏览器的requestAnimationFrame帧循环驱动结束后触发onComplete回调。README 中给出的标准用法如下来自 client/lib/scroll-to/README.mdimport scrollTo from calypso/lib/scroll-to; scrollTo( { x: 400, y: 500, duration: 500, onComplete: function () { console.log( done! ); }, } );在 wp-calypso 代码库中导入路径统一写作calypso/lib/scroll-to仓库内已有真实调用示例活动日志条目组件在 client/my-sites/activity/activity-log-item/index.jsx 中调用scrollTo( { x: 0, y: 0, duration: 250 } )将视口快速回滚到顶部client/components/infinite-list/index.jsx 也直接导入该模块用于无限列表的滚动恢复。二、API 参数全解从 index.js 中的 JSDoc 注释可以还原出该函数完整的选项接口参数类型默认值说明xnumber无目标左侧水平坐标ynumber无目标顶部垂直坐标easingFunctioncircularOutEasing缓动函数输入/输出均为归一化进度durationnumber500动画时长毫秒onStartFunction无动画开始前触发的回调onCompleteFunction无滚动到达目标位置后触发的回调containerHTMLElementwindow指定容器则滚动该容器而非整个窗口几个值得注意的实现细节对应 scrollTo 入口容器缺省为 windowconst container options.container || window所以不传container时滚动的就是整个页面这与浏览器原生window.scrollTo的行为一致返回值为 Stepper 实例函数末尾return stepper调用方可以拿到步进器对象自行调用其方法见下文jumpToonStart的触发时机它在stepper.animate()调度第一帧之后被同步调用即动画已排程但尚未真正移动时触发符合 JSDoc 中「callback before start is called」的约定。三、源码拆解Stepper 的帧循环时间步进整个模块的核心是 Stepper 类它不依赖任何第三方动画库纯靠requestAnimationFrame实现。3.1 调度与取消animate() { this.nextFrame requestAnimationFrame( this.step ); } cancel() { if ( this.nextFrame ) { cancelAnimationFrame( this.nextFrame ); this.nextFrame null; } if ( this.finishTimeout ) { clearTimeout( this.finishTimeout ); this.finishTimeout null; } }animate() 只负责把step注册到下一帧cancel() 则同时清理两类句柄requestAnimationFrame的帧句柄nextFrame和用于「跳变后延迟结束」的定时器finishTimeout。这种双句柄设计正是为下文的jumpTo抢占机制服务的。3.2 首帧取时间戳step ( ts ) { this.nextFrame null; if ( ! this.startTime ) { this.startTime ts; this.animate(); return; } // ... }step 方法 收到的ts是requestAnimationFrame回调参数DOMHighResTimeStamp。第一次进入时它不计算任何位移只是记录startTime并立刻再排一帧——这样保证了起点时间戳与渲染帧严格对齐而不是new Date()这类可能带调度误差的时间源。3.3 进度归一化与缓动插值// 是否到达/超过目标时长直接落到终点 if ( ts - this.startTime this.duration ) { this.finish(); return; } // 归一化到 (0,1) 的进度 const progress ( ts - this.startTime ) / this.duration; // 缓动函数可把进度变换出 [0,1] 范围但仍按比例插值 const easedProgress this.easing( progress ); // 起点 总距离 × 缓动进度 const newX Math.round( this.start.x ( this.end.x - this.start.x ) * easedProgress ); const newY Math.round( this.start.y ( this.end.y - this.start.y ) * easedProgress );这是典型的「起点 总位移 × 缓动进度」线性插值Lerp。两个工程细节值得记录坐标取整Math.round之后才写入滚动位置避免亚像素滚动值在部分浏览器中造成视觉抖动无变化不更新if ( newX ! this.x || newY ! this.y )才调用updater。源码注释解释得很直白——「短距离 长时长」场景下缓动后期每帧的插值结果可能完全相同跳过冗余写入可以减少滚动赋值带来的回流压力见 index.js L118-L125。3.4 收尾finishfinish () { this.updater( this.end.x, this.end.y ); if ( this.onComplete ) { this.onComplete(); } scrollers.delete( this.container ); };finish 做三件事强制把位置写成精确的目标值消除插值取整产生的最后 1px 误差、触发onComplete、并把该容器从全局scrollers表中移除释放「滚动进行中」的标记。四、默认缓动函数 circularOutEasing 的数学含义默认缓动 是内置的圆形缓出circular outfunction circularOutEasing( val ) { const inverse val - 1; return Math.sqrt( 1 - inverse * inverse ); }展开后即标准公式1 - sqrt(1 - t²)对inverse t - 1有inverse² (1-t)²可化简等价。其特征是t0时导数最大起步速度最快t1时导数为 0越接近目标速度越慢最终平滑停下这正是滚动到目标位置最自然的体感曲线——快速启程、减速停靠。JSDoc 注释中「Slows down as it approaches the target」描述的就是这一点。由于easing是普通函数参数调用方完全可以传入自己的缓动如线性、三次缓入缓出缓动函数的返回值允许超出[0,1]区间源码注释明确说明「Easing can transform our progress outside of the 0,1 range」插值公式对此同样成立因此回弹类缓动在数学上也是被支持的。五、window 与容器双模式getCurrentScroll 与 makeScrollUpdater模块对「滚动目标」做了统一抽象既可以是window也可以是任意具备scrollTop属性的容器元素如div、面板。5.1 读取当前位置function getCurrentScroll( container ) { if ( container container.scrollTop ! undefined ) { return { x: container.scrollLeft, y: container.scrollTop, }; } const x window.pageXOffset || document.documentElement.scrollLeft; const y window.pageYOffset || document.documentElement.scrollTop; return { x, y }; }getCurrentScroll 用container.scrollTop ! undefined作为容器判定条件容器存在且可滚就读容器的scrollLeft/scrollTop否则回落到窗口的pageXOffset/pageYOffset并做了||兜底老式浏览器中pageXOffset可能为 0/未定义时回退到documentElement.scrollLeft。5.2 写入位置function makeScrollUpdater( container ) { container container container.scrollTop ! undefined ? container : window; return function updateScroll( x, y ) { if ( container window ) { container.scrollTo( x, y ); } else { container.scrollTop y; container.scrollLeft x; } }; }makeScrollUpdater 是工厂函数为每个滚动目标闭包出一个updater(x, y)window 模式走window.scrollTo(x, y)容器模式直接赋值scrollTop/scrollLeft此处刻意不用container.scrollTo属性赋值是更古老、兼容性更稳的写法这种「读写配对」的抽象让 Stepper 对滚动目标完全无感知它是纯粹的时间步进器——这也是整个模块最清晰的职责分层Stepper 管时间updater 管坐标落地。六、同容器滚动抢占scrollers 表与 jumpTo模块顶部维护了一张按容器索引的全局步进器表const scrollers new Map();scrollTo 入口 的开头逻辑是全文最关键的设计决策const container options.container || window; if ( scrollers.has( container ) ) { const scroller scrollers.get( container ); scroller.jumpTo( options.x, options.y ); return; }即同一容器上同一时刻只允许存在一个滚动步进器。若上一次滚动还没结束又发起了新的scrollTo旧步进器不会销毁而是被jumpTo接管jumpTo( x, y ) { this.cancel(); this.end { x, y }; this.updater( x, y ); this.finishTimeout setTimeout( this.finish, this.duration ); }jumpTo 的行为语义是「瞬移 占位」cancel()取消旧动画的剩余帧循环目标坐标更新为新的{x, y}并立即把容器直接滚动到新位置不做过渡动画用setTimeout(this.finish, this.duration)占位duration毫秒到时才调用finish——而finish正是把容器从scrollers表里删掉的地方。这样做的工程意义在于防止高频场景如用户连点、列表项连续跳转中多个步进器交替驱动同一容器造成位置抖动同时占位期间新的scrollTo仍会命中scrollers.has(container)分支继续jumpTo行为保持一致。需要向读者说明的一点是jumpTo是瞬时跳变而非二次缓动它是抢占语义的一部分不要把它理解成「平滑地改道」。另外注意finish里的this.updater( this.end.x, this.end.y )与jumpTo里的this.updater( x, y )形成呼应无论哪条路径结束最终位置都等于目标坐标不存在「停在半路」的状态。七、单元测试如何验证该行为模块的测试位于 client/lib/scroll-to/test/index.js用jest-environment jsdom指定 jsdom 环境并在beforeAll中 mock 掉window.scrollTobeforeAll( () { jest.spyOn( window, scrollTo ).mockImplementation(); } );两条用例分别验证了垂直与水平方向test( window position x, () { return new Promise( ( done ) { scrollTo( { x: 500, y: 300, duration: 1, onComplete: () { expect( window.scrollTo ).toHaveBeenCalledWith( 500, 300 ); done(); }, } ); } ); } );测试手法上有两个要点duration: 1把动画压到 1ms让 jsdom 中的requestAnimationFrame循环几乎瞬时走完既避免 Promise 悬挂又保证断言在onComplete内同步成立断言落在终点坐标toHaveBeenCalledWith( 500, 300 )验证的正是finish阶段强制写入目标值的效果——即无论帧循环中间过程如何落地坐标必须精确等于入参。八、设计小结与使用注意事项结合源码可以总结该模块的几个工程特征零依赖、单文件约 180 行内聚实现client/lib/scroll-to/index.js只使用requestAnimationFrame、setTimeout与滚动属性无第三方缓动库JSDoc 提到 TWEEN 风格的缓动签名但实际缓动是内置的circularOutEasing窗口/容器同构通过container参数与「读写抽象」同一套 API 覆盖整页滚动与面板内滚动两种场景每容器单例步进器scrollersMap jumpTo抢占机制保证快速连续调用时的行为可预期最后一次调用说了算且立即生效使用注意x、y是绝对坐标相对目标滚动面左上角的偏移不是相对偏移量该模块是纯命令式 API不含 React 封装在 React 组件中使用时应放在useEffect/生命周期回调中触发如 activity-log-item 在回调中触发回顶滚动模块没有暴露公开的「取消滚动」APIcancel()是 Stepper 内部方法仅被jumpTo调用从源码结构看外部如需中止动画唯一受支持的路径是对同一容器再次scrollTo触发jumpTo。对于需要在 wp-calypso 中实现「平滑滚到某位置」的场景直接import scrollTo from calypso/lib/scroll-to并参考 README 示例 传参即可如需理解其动画时序、抢占语义或自定义缓动上文对 Stepper 帧循环、circularOutEasing 与 jumpTo 机制 的拆解即为完整依据。赞分享前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载相关推荐wp-calypso 中页面锚点平滑滚动机制详解scroll-to-anchor 模块实现剖析wp calypso 中页面锚点平滑滚动机制详解scroll to anchor 模块实现剖析 wp calypsoWordPress.com 的 Java前端CMSCuberto的平滑滚动(smooth-scroll)项目教程Cuberto的平滑滚动 smooth scroll 项目教程 1. 项目介绍 Cuberto的 smooth scroll https://github.cowp-calypso ActionCard 组件指南基于 Card 的 Call-to-Action 卡片实现与实战wp calypso ActionCard 组件指南基于 Card 的 Call to Action 卡片实现与实战 ActionCard 是 wp caly前端CMS上一篇日志分析新范式zincobserve一站式监控平台深度解析下一篇如何用seresnet50.a1_in1k实现高效图像分类3个核心步骤详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表