真正能在滚动时拿到光标“位置更新”同时又最轻量的做法,其实是不每次都去测 Selection → getClientRects() 或插入 marker,而是直接把上一次的坐标做“增量”调整。
核心思路:基于滚动偏移的增量更新
-
缓存上一次的绝对坐标
let lastPos = { x: /* 上次 computed x */, y: /* 上次 computed y */ }; let lastScroll = new Map(); // key: scrollable element, value: 上次 scrollTop/scrollLeft
-
初始化时,给每个可滚动容器记录初始 scrollTop/scrollLeft
scrollableEls.forEach(el => { lastScroll.set(el, { top: el.scrollTop, left: el.scrollLeft }); });
-
滚动事件里,只对 delta 做平移
function onScroll(e) { const el = e.target; const prev = lastScroll.get(el); const deltaY = el.scrollTop - prev.top; const deltaX = el.scrollLeft - prev.left; // 更新缓存 lastScroll.set(el, { top: el.scrollTop, left: el.scrollLeft }); // 在 transform 里直接做平移 lastPos.x -= deltaX; lastPos.y -= deltaY; customCursor.style.transform = `translate(${lastPos.x}px, ${lastPos.y}px)`; // 然后立刻用 lastPos 去检测是否超出“指定元素”边界 checkOutOfBounds(lastPos); }
-
其他需要精确定位的时刻(点击、输入、selectionchange)
function recalcCursor() { const pos = getStablePosition(); // 你的完整测量流程 if (pos) { lastPos = pos; customCursor.style.transform = `translate(${pos.x}px, ${pos.y}px)`; checkOutOfBounds(pos); } } document.addEventListener('click', recalcCursor); document.addEventListener('input', recalcCursor); document.addEventListener('selectionchange', recalcCursor);
这样:
- 滚动时:完全不打任何测量流程,只做简单的坐标减法,O(1) 的操作
- 点击/移动/输入时:才跑一次完整的
getStablePosition()
(只有用户真正改光标位置的场景才触发),保持高度和精度 - 出界检测:始终用当前的
lastPos
(滚动时同步平移后、或重新计算后)去判断是否超出指定容器的getBoundingClientRect()
边界即可
为什么这样最优?
- 滚动最频繁,用 O(1) 的平移替代 O(n) 的测量 + DOM 读写
- 既保证了即使光标在一个嵌套滚动容器内,也能第一时间同步移动坐标
- 你依然可以在任意时刻根据
lastPos
去判断“是否超出指定元素区域”,而不用隐藏再测 - 当用户真正把光标移到新位置(点击/输入/选择)时,再去跑完整精准的测量,保证不会累积误差
边界情况
- 如果页面同时存在多个滚动容器,你需要给它们都注册
scroll
监听,并维护各自的lastScroll
,delta 作用到同一套lastPos
- 如果滚动容器突然被重置(如内容刷新、重排),你可以在那次操作后 一并 调用
recalcCursor()
,以重新同步一次绝对坐标
这样,你就能在保证“任何时候都能检测出是否超出区域”的同时,把滚动时的性能开销降到最低:滚动里只做几行纯 JS 运算,无需任何布局读写。
