CSS & JS Effect – 用 wheel 模拟 scroll
前言
在 用 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的更多相关文章
- 模拟jQuery中的ready方法及实现按需加载css,js实例代码
		这篇文章介绍了模拟jQuery中的ready方法及实现按需加载css,js实例代码,有需要的朋友可以参考一下 一.ready函数的实现经常用jQuery类库或其他类库中的ready方法,有时候 ... 
- CSS & JS 制作滚动幻灯片
		==================纯CSS方式==================== <!DOCTYPE html> <html> <head> <met ... 
- css+js+html基础知识总结
		css+js+html基础知识总结 一.CSS相关 1.css的盒子模型:IE盒子模型.标准W3C盒子模型: 2.CSS优先级机制: 选择器的优先权:!important>style(内联样式) ... 
- jQuery中的ready方法及实现按需加载css,js
		模拟jQuery中的ready方法及实现按需加载css,js 一.ready函数的实现 经常用jQuery类库或其他类库中的ready方法,有时候想想它们到底是怎么实现的,但是看了一下jQuery中的 ... 
- 前端工程师面试问题归纳(一、问答类html/css/js基础)
		一.参考资源 1.前端面试题及答案整理(一) 2.2017年前端面试题整理汇总100题 3.2018最新Web前端经典面试试题及答案 4.[javascript常见面试题]常见前端面试题及答案 5.W ... 
- CSS&JS定位器
		一.CssSelector定位器 1.概述 CssSelector是效率很高的元素定位方法,Selenium官网的Document里极力推荐使用CSS locator,而不是XPath来定位元素,原因 ... 
- 前端小白页面开发注意事项及小工具(html\css\js)
		技术一直在向前发展.但是有一些是相通的,要找准重点,将80%的时间放在提升基础问题上,余下的20%再去学习框架,库和工具. HTML 1. HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读 ... 
- 第十五篇 JS 移入移出事件 模拟一个二级菜单
		JS 移入移出事件 模拟一个二级菜单 老师演示一个特别简单二级菜单,同学们除了学习JS,还要注意它的元素和CSS样式. 这节课介绍的是JS鼠标移入.移出事件:onmouseover是移入事件,on ... 
- html+css+js+Hbuilder开发一款安卓APP,根本不用学Android开发!
		我们知道,要做一款安卓APP,咱们得先学安卓开发语言,例如java,前端后端.那么没有这些开发语言基础,咱们怎么做呢?其实现在有比较好的开发方案就是做webAPP,咱们可以用web前端知识构建安卓客户 ... 
- 【转】Maven Jetty 插件的问题(css/js等目录死锁)的解决
		Maven Jetty 插件的问题(css/js等目录死锁,不能自动刷新)的解决: 1. 打开下面的目录:C:\Users\用户名\.m2\repository\org\eclipse\jetty ... 
随机推荐
- Elasticsearch  tp5使用
			下载elassticsearch和kibana的网址:https://www.elastic.co/cn/downloads/?elektra=home&store=hero 下载Elasti ... 
- CF1359A 题解
			洛谷链接&CF 链接 题目简述 共有 \(T\) 组数据. 对于每组数据给出 \(n,m,k\),表示 \(k\) 名玩家打牌,共 \(n\) 张牌,\(m\) 张王,保证 \(k \mid ... 
- Odoo 基于Win10搭建基于Win10搭建odoo14开发环境搭建
			实践环境 win10 Python 3.6.2 odoo_14.0.latest.tar.gz 下载地址: https://download.odoocdn.com/download/14/src?p ... 
- scratch源码下载 | 蜘蛛传说
			程序说明: <蜘蛛传说>是一个通过Scratch平台制作的互动游戏项目.在这个故事中,玩家将扮演一只蜘蛛,其原本和平的生活被一只入侵的壁虎所打破.为了保卫自己的家园,蜘蛛必须运用智慧和勇气 ... 
- scratch编程作品-龙年发大财
			作品介绍: 龙年欢歌而来,带着满满的希望与勃勃生机.愿小虎鲸Scratch资源站激发您编程之路的无限灵感,让每一天都充满探索与创造的喜悦.在这吉祥如意的年份里,愿您的每一份耕耘都换来丰收的喜悦,每一个 ... 
- SecureCRT通过vbs脚本实现自动化登录linux服务器
			1.配置登录主机名.用户和密码 2.配置登录后操作脚本目录 3.vbs操作脚本如下(crt也支持python) #$language = "VBScript" #$interfac ... 
- 我用Awesome-Graphs看论文:解读PowerGraph
			PowerGraph论文:<PowerGraph: Distributed Graph-Parallel Computation on Natural Graphs> 上次通过文章< ... 
- 【爬虫】Java爬取KFC全国门店信息
			官网地址: http://www.kfc.com.cn/kfccda/storelist/index.aspx 基础库 <dependencies> <dependency> ... 
- 【Vue2】Vue-Cli使用
			1.需要NodeJS环境支持,此处省略NodeJS安装 2.使用NPM命令安装CLI包 vue-cli是npm.上的一个全局包,使用npm install 命令,即可方便的把它安装到自己的电脑上: n ... 
- 【RabbitMQ】02 工作队列模式
			首先编写一个工作队列的生产者: 发送10条消息然后就关闭,10条消息让RabbitMQ先存着 import com.rabbitmq.client.Channel; import com.rabbit ... 
