官方文档地址:
http://ustbhuangyi.github.io/better-scroll/doc/zh-hans/
应用场景:列表滚动
版本:1.15.2
用法:
我们先来看一下它的 html 结构:
<section class="menu_container">
/* wrapper层级 */
<div class="wrapper" ref="menuList">
/* scroller层级.此层级内容的高度必须大于wrapper,才能滚动 */
<ul class="content">
<li>...</li>
<li>...</li>
...
</ul>
</div>
</section>
less代码如下:
.menu_container {
position: absolute;
left: 0;
top: 0;
bottom: 0;
overflow: hidden;
display: flex;
.wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
better-scroll 是作用在外层 wrapper 容器上的,滚动的部分是 content 元素。这里要注意的是,better-scroll 只处理容器(wrapper)的第一个子元素(content)的滚动,其它的元素都会被忽略。
script代码如下:
<script>
import BScroll from "better-scroll";
export default {
data() {
return {
showLoading: true, //加载动画
bScroll: null,
};
},
mounted() {
this.initData();
},
methods: {
async initData() {
...
this.hideLoading();
this.$nextTick(() => {
// DOM 现在更新了
this.initBScroll();
});
},
hideLoading() {
this.showLoading = false;
},
initBScroll() {
if (!this.bScroll) {
this.bScroll = new BScroll(this.$refs.menuList, {
mouseWheel: true,
probeType: 3, //有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。如果没有设置该值,其默认值为 0,即不派发 scroll 事件。
click: true //better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。
});
}
}
},
watch: {
showLoading: function(value) {
if (!value) {
this.$nextTick(() => {
if (this.bScroll) {
this.bScroll.refresh();
}
});
}
}
}
};
</script>
better-scroll滚动无效的原因
https://blog.csdn.net/qiqi_77_/article/details/79361413
可在这里一一排除这几个情况。
下面开始分析大神的源码~
源码版本:0.1.15;此版本相对比较容易理解,后面会逐渐阅读最新源码。
构造函数
export class BScroll extends EventEmitter {
constructor(el, options) {
super();
this.wrapper = typeof el === 'string' ? document.querySelector(el) : el;
this.scroller = this.wrapper.children[0];
this.scrollerStyle = this.scroller.style;
this.options = {
startX: 0,
startY: 0,
scrollY: true,/* 默认开启纵向滚动 */
bounce: true,/* 当滚动超过边缘的时候会有一小段回弹动画。设置为 true 则开启动画 */
bounceTime: 800,/* 设置回弹动画的动画时长 */
resizePolling: 60,/* 当窗口的尺寸改变的时候,需要对 better-scroll 做重新计算,为了优化性能,我们对重新计算做了延时。60ms 是一个比较合理的值 */
preventDefault: true,/* 当事件派发后是否阻止浏览器默认行为 */
preventDefaultException: {
tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/
},/* better-scroll 的实现会阻止原生的滚动,这样也同时阻止了一些原生组件的默认行为。这个时候我们不能对这些元素做 preventDefault,所以我们可以配置 preventDefaultException。默认值 {tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/}表示标签名为 input、textarea、button、select 这些元素的默认行为都不会被阻止。 */
useTransition: true,/* 是否使用 CSS3 transition 动画。如果设置为 false,则使用 requestAnimationFrame 做动画 */
useTransform: true,/* 是否使用 CSS3 transform 做位移。如果设置为 false, 则设置元素的 top/left (这种情况需要 scroller 是绝对定位的) */
wheel: false,/* 这个配置是为了做 Picker 组件用的,默认为 false,如果开启则需要配置一个 Object。 */
momentum: true,/* 当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画。设置为 true 则开启动画 */
momentumLimitTime: 300,/* 只有在屏幕上快速滑动的时间小于 momentumLimitTime,才能开启 momentum 动画 */
momentumLimitDistance: 15,/* 只有在屏幕上快速滑动的距离大于 momentumLimitDistance,才能开启 momentum 动画 */
// let {deceleration, swipeBounceTime, bounceTime} = options;
swipeTime: 2500,/* 设置 momentum 动画的动画时长 */
// deceleration: 0.001,/* 表示 momentum 动画的减速度 */
deceleration: 0.0015,
swipeBounceTime: 500,/* 设置当运行 momentum 动画时,超过边缘后的回弹整个动画时间 */
}
/* 初始化并addEventListener */
this._init();
/* 判断页面是否可以滚动及初始化页面位置 */
this.refresh();
}
...
}
可以看出BScroll继承自EventEmitter,即一个发布订阅模式的类。作者借此类实现滚动状态的监听。关于EventEmitter 可以查看https://www.jianshu.com/p/2ed4684cca77
/* DOM 事件触发 */
handleEvent(e) {
switch (e.type) {
case 'touchstart':
case 'mousedown':
this._start(e);
break;
case 'touchmove':
case 'mousemove':
this._move(e);
break;
case 'touchend':
case 'mouseup':
case 'touchcancle':
case 'mousecancle':
this._end(e);
break;
case 'transitionend':
case 'webkitTransitionEnd':
case 'oTransitionEnd':
case 'MSTransitionEnd':
this._transitionEnd(e);
break;
}
}
用户开始触摸时触发
_start(e) {
...
if (this.options.preventDefault && !isBadAndroid && !preventDefaultException(e.target, this.options.preventDefaultException)) {
e.preventDefault();//阻止页面滚动
}
/* 此时move为false */
this.moved = false;
/* 滚动总距离 */
this.distX = 0;
this.distY = 0;
this._transitionTime();
/* 开始触摸时间 */
this.startTime = +new Date();
/* 如果页面滚动过程中又有新的触屏或者滚动操作 */
if (this.options.useTransition && this.isInTransition) {
/* 停止旧的滚动操作 */
this.isInTransition = false;
/* 获取滚动的位置坐标 */
let pos = this.getComputedPosition();
/* 页面位置坐标置为pos */
this._translate(pos.x, pos.y);
this.trigger('scrollEnd');
}
/* 初始化位置信息 */
let point = e.touches ? e.touches[0] : e;
this.startX = this.x;
this.startY = this.y;
this.absStartX = this.x;
this.absStartY = this.y;
/* pageX和pageY:获取鼠标指针距离文档(HTML)的左上角距离,不会随着滚动条滚动而改变 */
this.pointX = point.pageX;
this.pointY = point.pageY;
this.trigger('beforeScrollStart');
}
用户移动触摸点时触发
_move(e) {
...
if (this.options.preventDefault) {
/* 阻止屏幕的touchmove,mousemove事件 */
e.preventDefault();
}
/* 记录一段滚动之间的间隔距离 deltaX deltaY */
let point = e.touches ? e.touches[0] : e;
let deltaX = point.pageX - this.pointX;
let deltaY = point.pageY - this.pointY;
this.pointX = point.pageX;
this.pointY = point.pageY;
/* 计算滚动总距离 */
this.distX += deltaX;
this.distY += deltaY;
let absDistX = Math.abs(this.distX);
let absDistY = Math.abs(this.distY);
let timestamp = +new Date();
// We need to move at least 15 pixels for the scrolling to initiate
/* 如果滑动时间过长 及 距离过短,打断此次滑动事件 */
if (timestamp - this.endTime > this.options.momentumLimitTime && (absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance)) {
return;
}
deltaX = this.hasHorizontalScroll ? deltaX : 0;
deltaY = this.hasVerticalScroll ? deltaY : 0;
/* 计算最新的滚动位置 */
let newX = this.x + deltaX;
let newY = this.y + deltaY;
//如果滑动超出了界限,就减速或停止
if (newX > 0 || newX < this.maxScrollX) {
if (this.options.bounce) {
newX = this.x + deltaX / 3;
} else {
newX = newX > 0 ? 0 : this.maxScrollX;
}
}
if (newY > 0 || newY < this.maxScrollY) {
if (this.options.bounce) {
newY = this.y + deltaY / 3;
} else {
newY = newY > 0 ? 0 : this.maxScrollY;
}
}
if (!this.moved) {
this.moved = true;
this.trigger('scrollStart');
}
/* 将页面滚动到最新位置 */
this._translate(newX, newY);
if (timestamp - this.startTime > this.options.momentumLimitTime) {
/* 如果手指拖动时间过长,更新开始时间及坐标 */
this.startTime = timestamp;
this.startX = this.x;
this.startY = this.y;
}
if (this.options.probeType > 1) {
this.trigger('scroll', {
x: this.x,
y: this.y
});
}
let scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft;
let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
let pX = this.pointX - scrollLeft;
let pY = this.pointY - scrollTop;
/* 当手指一直往上或者往下拖动到距离屏幕边缘momentumLimitDistance(即15像素)处,停止move */
if (pX > document.documentElement.clientWidth - this.options.momentumLimitDistance || pX < this.options.momentumLimitDistance || pY < this.options.momentumLimitDistance || pY > document.documentElement.clientHeight/*屏幕高度*/ - this.options.momentumLimitDistance
) {
this._end(e);
}
}
触摸点取消时触发
_end(e) {
...
if (this.options.preventDefault && !preventDefaultException(e.target, this.options.preventDefaultException)) {
/* 阻止默认的end事件 */
e.preventDefault();
}
/* 如果在边界之外重置 */
if (this.resetPosition(this.options.bounceTime, ease.bounce)) {
return;
}
this.isInTransition = false;
/* 确保最后一个位置是四舍五入的 */
let newX = Math.round(this.x);
let newY = Math.round(this.y);
if (!this.moved) {
/* 滚动距离非常少或者点击动作会触发此操作 */
this.trigger('scrollCancle');
return;
}
this.scrollTo(newX, newY);
this.endTime = +new Date();
let duration = this.endTime - this.startTime;
let absDistX = Math.abs(newX - this.startX);
let absDistY = Math.abs(newY - this.startY);
let time = 0;
// start momentum animation if needed 短时间内移动距离大于300,启动动量动画
if (this.options.momentum && duration < this.options.momentumLimitTime && (absDistY > this.options.momentumLimitDistance || absDistX > this.options.momentumLimitDistance)) {
let momentumX = this.hasHorizontalScroll ? momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options)
: { destination: newX, duration: 0 };
let momentumY = this.hasVerticalScroll ? momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options)
: { destination: newY, duration: 0 };
newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumX.duration, momentumY.duration);
this.isInTransition = 1;
}
let easing = ease.swipe;
...
/* 将页面滚动到最新位置 */
this.scrollTo(newX, newY, time, easing);
}
transform动画执行结束后触发
_transitionEnd(e) {
if (e.target !== this.scroller || !this.isInTransition) {
return;
}
this._transitionTime();
/* 动画执行结束后,如果动量动画或者手动滚动越界,重置位置 */
if (!this.resetPosition(this.options.bounceTime, ease.bounce)) {
this.isInTransition = false;
this.trigger('scrollEnd');
}
}
此时,一个完整的滚动操作就已经完结了。
通过这个滚动操作可以看出核心原理,是阻止页面的系统滚动,添加事件监听,在各个dom事件中处理用户滚动事件,通过手动transform将页面移动到合适的位置。
网友评论