前言

用 JavaScript 实现 position sticky 文章中,我提到了用 wheel 来模拟 scroll 效果。

这篇来说说具体怎么实现,挺简单的哦。

Preparation

table.html

<div class="container">
<table>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Age</th>
<th>Address</th>
<th>Email</th>
<th>Phone</th>
<th>City</th>
<th>Country</th>
<th>Occupation</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>Doe</td>
<td>30</td>
<td>123 Main St</td>
<td>john.doe@example.com</td>
<td>123-456-7890</td>
<td>New York</td>
<td>USA</td>
<td>Software Engineer</td>
<td>$80,000</td>
</tr>
<tr>
<td>Jane</td>
<td>Smith</td>
<td>25</td>
<td>456 Elm St</td>
<td>jane.smith@example.com</td>
<td>987-654-3210</td>
<td>Los Angeles</td>
<td>USA</td>
<td>Graphic Designer</td>
<td>$60,000</td>
</tr>
<tr>
<td>Michael</td>
<td>Johnson</td>
<td>35</td>
<td>789 Oak St</td>
<td>michael.johnson@example.com</td>
<td>456-789-0123</td>
<td>Chicago</td>
<td>USA</td>
<td>Teacher</td>
<td>$50,000</td>
</tr>
<tr>
<td>Sarah</td>
<td>Williams</td>
<td>28</td>
<td>321 Pine St</td>
<td>sarah.williams@example.com</td>
<td>789-012-3456</td>
<td>Miami</td>
<td>USA</td>
<td>Accountant</td>
<td>$70,000</td>
</tr>
<tr>
<td>David</td>
<td>Brown</td>
<td>40</td>
<td>654 Cedar St</td>
<td>david.brown@example.com</td>
<td>210-987-6543</td>
<td>Houston</td>
<td>USA</td>
<td>Engineer</td>
<td>$90,000</td>
</tr>
<tr>
<td>Emily</td>
<td>Miller</td>
<td>33</td>
<td>987 Maple St</td>
<td>emily.miller@example.com</td>
<td>567-890-1234</td>
<td>Seattle</td>
<td>USA</td>
<td>Manager</td>
<td>$100,000</td>
</tr>
<tr>
<td>James</td>
<td>Wilson</td>
<td>27</td>
<td>753 Walnut St</td>
<td>james.wilson@example.com</td>
<td>890-123-4567</td>
<td>San Francisco</td>
<td>USA</td>
<td>Marketing Specialist</td>
<td>$75,000</td>
</tr>
<tr>
<td>Emma</td>
<td>Anderson</td>
<td>29</td>
<td>159 Birch St</td>
<td>emma.anderson@example.com</td>
<td>234-567-8901</td>
<td>Boston</td>
<td>USA</td>
<td>Consultant</td>
<td>$85,000</td>
</tr>
<tr>
<td>Christopher</td>
<td>Lee</td>
<td>32</td>
<td>852 Oakwood St</td>
<td>christopher.lee@example.com</td>
<td>678-901-2345</td>
<td>Atlanta</td>
<td>USA</td>
<td>Lawyer</td>
<td>$120,000</td>
</tr>
<tr>
<td>Olivia</td>
<td>Clark</td>
<td>26</td>
<td>357 Elmwood St</td>
<td>olivia.clark@example.com</td>
<td>123-456-7890</td>
<td>Denver</td>
<td>USA</td>
<td>Artist</td>
<td>$55,000</td>
</tr>
<tr>
<td>William</td>
<td>White</td>
<td>31</td>
<td>951 Cedarwood St</td>
<td>william.white@example.com</td>
<td>456-789-0123</td>
<td>Phoenix</td>
<td>USA</td>
<td>Architect</td>
<td>$95,000</td>
</tr>
<tr>
<td>Ava</td>
<td>Hall</td>
<td>34</td>
<td>246 Pinecrest St</td>
<td>ava.hall@example.com</td>
<td>789-012-3456</td>
<td>Dallas</td>
<td>USA</td>
<td>Financial Analyst</td>
<td>$80,000</td>
</tr>
<tr>
<td>Alexander</td>
<td>Young</td>
<td>29</td>
<td>753 Maplewood St</td>
<td>alexander.young@example.com</td>
<td>210-987-6543</td>
<td>Philadelphia</td>
<td>USA</td>
<td>Real Estate Agent</td>
<td>$70,000</td>
</tr>
<tr>
<td>Mia</td>
<td>Scott</td>
<td>38</td>
<td>852 Oak St</td>
<td>mia.scott@example.com</td>
<td>567-890-1234</td>
<td>Minneapolis</td>
<td>USA</td>
<td>Doctor</td>
<td>$150,000</td>
</tr>
<tr>
<td>Ethan</td>
<td>Adams</td>
<td>27</td>
<td>369 Walnut St</td>
<td>ethan.adams@example.com</td>
<td>890-123-4567</td>
<td>Portland</td>
<td>USA</td>
<td>Journalist</td>
<td>$65,000</td>
</tr>
<tr>
<td>Isabella</td>
<td>Carter</td>
<td>30</td>
<td>147 Pine St</td>
<td>isabella.carter@example.com</td>
<td>234-567-8901</td>
<td>Detroit</td>
<td>USA</td>
<td>Entrepreneur</td>
<td>$200,000</td>
</tr>
<tr>
<td>Logan</td>
<td>Green</td>
<td>31</td>
<td>258 Elm St</td>
<td>logan.green@example.com</td>
<td>678-901-2345</td>
<td>San Diego</td>
<td>USA</td>
<td>Engineer</td>
<td>$90,000</td>
</tr>
<tr>
<td>Amelia</td>
<td>Roberts</td>
<td>29</td>
<td>369 Cedar St</td>
<td>amelia.roberts@example.com</td>
<td>123-456-7890</td>
<td>Charlotte</td>
<td>USA</td>
<td>Designer</td>
<td>$70,000</td>
</tr>
<tr>
<td>Benjamin</td>
<td>Hill</td>
<td>35</td>
<td>741 Oakwood St</td>
<td>benjamin.hill@example.com</td>
<td>456-789-0123</td>
<td>San Antonio</td>
<td>USA</td>
<td>Manager</td>
<td>$100,000</td>
</tr>
<tr>
<td>Charlotte</td>
<td>Adams</td>
<td>33</td>
<td>852 Maple St</td>
<td>charlotte.adams@example.com</td>
<td>789-012-3456</td>
<td>Orlando</td>
<td>USA</td>
<td>Software Developer</td>
<td>$85,000</td>
</tr>
<tr>
<td>Gabriel</td>
<td>Cook</td>
<td>28</td>
<td>159 Pinecrest St</td>
<td>gabriel.cook@example.com</td>
<td>210-987-6543</td>
<td>Tampa</td>
<td>USA</td>
<td>Writer</td>
<td>$60,000</td>
</tr>
</tbody>
</table>
</div>

table.scss

.container {
max-height: 256px;
overflow-y: auto;
max-width: 768px;
margin-inline: auto;
} table {
border-spacing: 0;
margin-inline: auto; th,
td {
border: 1px solid black;
} :is(th, td):nth-child(n + 2) {
border-left: unset;
} td {
border-top: unset;
} thead {
tr {
background-color: white;
} th {
padding: 16px;
}
} td,
th {
padding: 16px;
min-width: 250px;
max-width: 250px;
}
}

实现原理

监听 wheel 事件,会得到一个 WheelEvent 对象。

它里面有一个 deltaY 属性,我们 wheel 一下,这个 deltaY 会是 100 或 -100。

positive 表示 scroll down,negative 表示 scroll up。

100 是游览器设定的一下 wheel 要移动多少 scrollTop。

轻轻 wheel 一下就是 scrollTop += 100

如果快速 wheel 几下,这个 deltaY 不一定是 100,有可能是 200 甚至 300。

也就是说 wheel 的越快越多,移动的 scrollTop 越大。这是游览器的交互体验。

我们监听 wheel 然后 update scrollTop 就可以了。如果要体验好,就加入 animation,让它 smooth 一点。

具体实现代码

table.ts

我用了 RxJS,不熟悉的朋友可以参考:RxJS 系列

const container = document.querySelector<HTMLElement>('.container')!;
// 监听 wheel 事件
const wheel$ = fromEvent<WheelEvent>(container, 'wheel').pipe(share());
// preventDefault body scroll,因为我们要控制的是 div scroll
wheel$.subscribe(e => e.preventDefault());
// 从 event 取出 deltaY
const deltaY$ = wheel$.pipe(map(e => e.deltaY)).pipe(share());
// 区分出 scroll up 和 scroll down
const [scrollUp$, scrollDown$] = partition(deltaY$, deltaY => deltaY < 0);
// for loop subscribe scroll$
for (const scroll$ of [scrollUp$, scrollDown$]) {
scroll$
.pipe(
// 下面 scroll 指的是 要 scrollTop 多少
// 轻轻 wheel 一下,scrollPerWheel 是 100
// 快快 wheel 的话,scrollPerWheel 可能会去到 200, 300
mergeMap(scrollPerWheel => {
// 如果是 scroll up,scrollPerWheel 会是 negative,我们为了统一算法,把它变成 positive 会比较方便
if (scroll$ === scrollUp$) scrollPerWheel *= -1;
// 150ms 内要完成 scroll
const duration = 150;
// 每一 ms 要 scroll 多少?
const scrollPerMillisecond = scrollPerWheel / duration; return animationFrames().pipe(
// animationFrames 就是递归调用 requestAnimatonFrame
// elapsed 是一个累加的 ms
map(e => e.elapsed),
startWith(0),
pairwise(),
// 通过 current elapsed 减去 previous elapsed 就可以直到这一次的 requestAnimatonFrame 间隔多少时间
// 游览器 requestAnimatonFrame 通常间隔是在 16ms 左右,但也不太准,所以我们还是得准确算一下
map(([prev, curr]) => curr - prev),
scan(
({ totalScroll }, animationInterval) => {
// 每 16ms 左右我们就会 scroll 一点点
// 一直到 scroll 到 100px 就停
// remainingScroll 就是一个从 100 一直累减到 0 的记入
const remainingScroll = scrollPerWheel - totalScroll;
// 计算这一次要 scroll 多好
const scroll = limitMax(Math.ceil(animationInterval * scrollPerMillisecond), remainingScroll);
// totalScroll 则是已经 scroll 了多少
return { totalScroll: totalScroll + scroll, lastScroll: scroll };
},
{ totalScroll: 0, lastScroll: 0 },
),
// 判断 totalScroll 满了就停
takeWhile(({ totalScroll }) => totalScroll !== scrollPerWheel, true),
// 如果是 scroll up 要把它转换回 negative
map(e => (scroll$ === scrollDown$ ? e.lastScroll : e.lastScroll * -1)),
// 150ms 内如果用户反方向 wheel 就立刻停止以前方向的 scroll
takeUntil(scroll$ === scrollDown$ ? scrollUp$ : scrollDown$),
);
}),
)
// 每一次修改 scrollTop
.subscribe(scroll => (container.scrollTop += scroll));
}

效果

和原生的不会差太远,够用。

如果还想加入 overscroll 概念,可以添加一个 targetScrollElement$,它会决定要 scroll 哪一个 element (child to ancestor)

// 当用户停止 wheel 之后的第一个 wheel 做检查
// 这里使用 debounceTime 300ms 来等待用户停止 wheel
const targetScrollElement$ = wheel$.pipe(debounceTime(300), startWith(null)).pipe(
switchMap(() => {
return wheel$.pipe(
map(e => {
const upOrDown = e.deltaY > 0 ? 'Down' : 'Up';
// scrollElements 是 child to ancestor element
return scrollElements.find((scrollElement, index) => {
// 如果已经是最后一个 element 直接返回就好,总要有人可以 scroll 嘛
if (index === scrollElements.length - 1) return true;
// 如果要 scroll up 同时还没有 scroll 到顶就可以 scroll 这个 element
if (upOrDown === 'Up' && !reachedTop(scrollElement)) return true;
// 如果要 scroll down 同时还没有 scroll 到底就可以 scroll 这个 element
if (upOrDown === 'Down' && !reachedBottom(scrollElement)) return true;
// 不可以就去检查下一个 parent
return false;
})!;
}),
take(1), // 检查一次就行了
); // 判断是否已经 scroll 到顶部
function reachedTop(element: HTMLElement) {
return element.scrollTop === 0;
} // 判断是否已经 scroll 到底部
function reachedBottom(element: HTMLElement) {
return element.scrollHeight - element.clientHeight === element.scrollTop;
}
}),
shareReplay(1),
);

完整代码

export function setupWheelToScroll(
wheelElement: HTMLElement,
scrollElement: HTMLElement | HTMLElement[],
): Subscription {
const duration = 150; const scrollElements = Array.isArray(scrollElement) ? scrollElement : [scrollElement];
const subscription = new Subscription(); const wheel$ = fromEvent<WheelEvent>(wheelElement, 'wheel').pipe(share());
subscription.add(wheel$.subscribe(e => e.preventDefault())); // 当用户停止 wheel 之后的第一个 wheel 做检查
// 这里使用 debounceTime 300ms 来等待用户停止 wheel
const targetScrollElement$ = wheel$.pipe(debounceTime(300), startWith(null)).pipe(
switchMap(() => {
return wheel$.pipe(
map(e => {
const upOrDown = e.deltaY > 0 ? 'Down' : 'Up';
// scrollElements 是 child to parent element
return scrollElements.find((scrollElement, index) => {
// 如果已经是最后一个 element 直接返回就好,总要有人可以 scroll 嘛
if (index === scrollElements.length - 1) return true;
// 如果要 scroll up 同时还没有 scroll 到顶就可以 scroll 这个 element
if (upOrDown === 'Up' && !reachedTop(scrollElement)) return true;
// 如果要 scroll down 同时还没有 scroll 到底就可以 scroll 这个 element
if (upOrDown === 'Down' && !reachedBottom(scrollElement)) return true;
// 不可以就去检查下一个 parent
return false;
})!;
}),
take(1), // 检查一次就行了
); // 判断是否已经 scroll 到顶部
function reachedTop(element: HTMLElement) {
return element.scrollTop === 0;
} // 判断是否已经 scroll 到底部
function reachedBottom(element: HTMLElement) {
return element.scrollHeight - element.clientHeight === element.scrollTop;
}
}),
shareReplay(1),
); const deltaY$ = wheel$.pipe(map(e => e.deltaY)).pipe(share());
const [scrollUp$, scrollDown$] = partition(deltaY$, deltaY => deltaY < 0); for (const scroll$ of [scrollUp$, scrollDown$]) {
const scrollSub = scroll$
.pipe(
mergeMap(scrollPerWheel => {
if (scroll$ === scrollUp$) scrollPerWheel *= -1; const scrollPerMillisecond = scrollPerWheel / duration; return animationFrames().pipe(
map(e => e.elapsed),
startWith(0),
pairwise(),
map(([prev, curr]) => curr - prev),
scan(
({ totalScroll }, animationInterval) => {
const remainingScroll = scrollPerWheel - totalScroll;
const scroll = limitMax(Math.ceil(animationInterval * scrollPerMillisecond), remainingScroll);
return { totalScroll: totalScroll + scroll, lastScroll: scroll };
},
{ totalScroll: 0, lastScroll: 0 },
),
takeWhile(({ totalScroll }) => totalScroll !== scrollPerWheel, true),
map(e => (scroll$ === scrollDown$ ? e.lastScroll : e.lastScroll * -1)),
takeUntil(scroll$ === scrollDown$ ? scrollUp$ : scrollDown$),
);
}),
withLatestFrom(targetScrollElement$),
)
.subscribe(([scroll, targetScrollElement]) => (targetScrollElement.scrollTop += scroll));
subscription.add(scrollSub);
}
return subscription;
}

CSS & JS Effect – 用 wheel 模拟 scroll的更多相关文章

  1. 模拟jQuery中的ready方法及实现按需加载css,js实例代码

    这篇文章介绍了模拟jQuery中的ready方法及实现按需加载css,js实例代码,有需要的朋友可以参考一下     一.ready函数的实现经常用jQuery类库或其他类库中的ready方法,有时候 ...

  2. CSS & JS 制作滚动幻灯片

    ==================纯CSS方式==================== <!DOCTYPE html> <html> <head> <met ...

  3. css+js+html基础知识总结

    css+js+html基础知识总结 一.CSS相关 1.css的盒子模型:IE盒子模型.标准W3C盒子模型: 2.CSS优先级机制: 选择器的优先权:!important>style(内联样式) ...

  4. jQuery中的ready方法及实现按需加载css,js

    模拟jQuery中的ready方法及实现按需加载css,js 一.ready函数的实现 经常用jQuery类库或其他类库中的ready方法,有时候想想它们到底是怎么实现的,但是看了一下jQuery中的 ...

  5. 前端工程师面试问题归纳(一、问答类html/css/js基础)

    一.参考资源 1.前端面试题及答案整理(一) 2.2017年前端面试题整理汇总100题 3.2018最新Web前端经典面试试题及答案 4.[javascript常见面试题]常见前端面试题及答案 5.W ...

  6. CSS&JS定位器

    一.CssSelector定位器 1.概述 CssSelector是效率很高的元素定位方法,Selenium官网的Document里极力推荐使用CSS locator,而不是XPath来定位元素,原因 ...

  7. 前端小白页面开发注意事项及小工具(html\css\js)

    技术一直在向前发展.但是有一些是相通的,要找准重点,将80%的时间放在提升基础问题上,余下的20%再去学习框架,库和工具. HTML 1. HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读 ...

  8. 第十五篇 JS 移入移出事件 模拟一个二级菜单

    JS 移入移出事件 模拟一个二级菜单   老师演示一个特别简单二级菜单,同学们除了学习JS,还要注意它的元素和CSS样式. 这节课介绍的是JS鼠标移入.移出事件:onmouseover是移入事件,on ...

  9. html+css+js+Hbuilder开发一款安卓APP,根本不用学Android开发!

    我们知道,要做一款安卓APP,咱们得先学安卓开发语言,例如java,前端后端.那么没有这些开发语言基础,咱们怎么做呢?其实现在有比较好的开发方案就是做webAPP,咱们可以用web前端知识构建安卓客户 ...

  10. 【转】Maven Jetty 插件的问题(css/js等目录死锁)的解决

    Maven Jetty 插件的问题(css/js等目录死锁,不能自动刷新)的解决:   1. 打开下面的目录:C:\Users\用户名\.m2\repository\org\eclipse\jetty ...

随机推荐

  1. Spring MVC 中 HttpMessageConverter 转换器

    1. Spring MVC 中 HttpMessageConverter 转换器 @ 目录 1. Spring MVC 中 HttpMessageConverter 转换器 2. 补充:什么是 HTT ...

  2. 史上最详细的Composer安装tp5教程

    Composer安装tp5教程1.下载composer先介绍几个网站Composer官网https://getcomposer.org/ Composer中文网http://www.phpcompos ...

  3. php页面调用微信扫一扫

    function.php <?php define("appID", "微信公众号appId"); define("appsecret" ...

  4. oeasy教您玩转vim - 68 - # 标签页tab

    ​ tab选项卡 回忆上次 上次有三种批量替换,分别是 :windo :bufdo :argdo 执行的{cmd}可以用|按顺序增加 分别对应的 windows 窗口 buffers 缓存文件 arg ...

  5. vue小知识~注入provide!

    注入表示的是将该组件的相关值,方法,实例向后代组件注入. 祖先元素中定义注入: export default { provide() { return { provideName: provideVa ...

  6. ssh 转发 和 切换图形化

    适用环境 宿主机连接到一台服务器是,服务器系统里面的浏览器点击http网页卡顿,那么这时可以通过ssh将端口转发到宿主机 使用宿主机的浏览器点击,则不会很卡顿. [root@foundation1 ~ ...

  7. Vue Hook 封装通用型表格

    一.创建通用型表格的需求 实现一个通用型表格组件,具备以下功能: 动态列配置. 分页功能. 排序功能. 可扩展的行操作功能. 二.设计通用型表格组件 首先,需要设计一个基础的表格组件,它接受列配置.数 ...

  8. SEO自动外链蜘蛛池工具促进百度快速收录怎么样 跟大家详谈一下

    此工具集成市面上所有自动外链网站的资源链接,经过合并.去重.筛选.验证 总结出最终的外链资源 ,软件实时更新 本软件将您繁杂的外链推广转为自动化进行,并且加入站群的支持,您只需要将你的站群域名粘贴到软 ...

  9. Spring启动报8080端口被占用问题

    1.window下关闭8080端口 win+R:输入cmd,回车 在黑窗口中输入指令:netstat -ano | findstr 8080       指令的意思是找出占用8080端口的进程pid ...

  10. net8实现MediatR小示例C#

    MediatR是.net下的一个实现消息传递的库,简洁高效,它采用中介者设计模式,通过进程内消息传递机制,进行请求/响应.命令.查询.通知和事件的消息传递,可通过泛型来支持消息的智能调度,用于领域事件 ...