整体结构
<transition name="el-fade-in">
<div
v-if="visible"
@click.stop="handleClick"
:style="{
'right': styleRight,
'bottom': styleBottom
}"
class="el-backtop">
<slot>
<el-icon name="caret-top"></el-icon>
</slot>
</div>
</transition>
可以看到,backtop组件的结构是比较简单的。div > icon。
icon是写在slot里面,这样子的写法有一个好处就是如果没有传递slot,那么这个icon将会显示出来,如果传递slot,那么icon将不会渲染,详见官网:cn.vuejs.org/v2/guide/co…
主要功能
backTop组件有两个重要的点
- visible: 何时显示
- click事件: 点击返回顶部
如果我们完成这两个功能 ,backTop基本就算完成了。
1. visible
mounted() {
this.init();
this.throttledScrollHandler = throttle(300, this.onScroll);
this.container.addEventListener('scroll', this.throttledScrollHandler);
},
// methods:
init() {
this.container = document;
this.el = document.documentElement;
if (this.target) {
this.el = document.querySelector(this.target);
if (!this.el) {
throw new Error(`target is not existed: ${this.target}`);
}
this.container = this.el;
}
},
mounted时,获取prop值target(触发滚动的对象),将target元素赋值给this.el和this.container。监听this.container的scroll事件。同时,这里做了节流的处理。每300毫秒内重复触发scroll事件都会被认为只触发一次 。
我觉得有两个节流与防抖的比喻,非常形象!!!
节流好比地铁限流,过一段时间才会放人进去
防抖好比坐电梯,只有没有人在上来了,电梯门才会关闭。否则持续有人上电梯,电梯是不会关门的。
当滚动的高度大于visibilityHeight(backtop组件可见时的高度), visible才会为true。
2. click
click事件除了emit之外,还要做的就是返回顶部。如果直接将this.el的滚动高度设置为0,当然是最简单最容易达到目的。但是不要忘了,element-ui的设计原则: 一致,反馈,效率,可控。feedback(反馈)这一点,明确的说到:
反馈 Feedback
控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作; 页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。
如果我们能够让滚动条慢慢的滚回到顶部,而不是一下子跳到顶部,是比较符合我们的习惯的。show code:
scrollToTop() {
let el = this.el;
let step = 0;
let interval = setInterval(() => {
if (el.scrollTop <= 0) {
clearInterval(interval);
return;
}
step += 10;
el.scrollTop -= step;
}, 20);
}
attention please:
- 这里并不是匀速往上滑动,而是越来越快,每隔20ms,scrollTop减去的step都会加10。其实也很好理解,如果我们的页面非常长,滚动条处于底部,我们不希望它像老人车过斑马线一样慢吞吞的过去。
- 这样子减会有一个问题,如果减到
scrollTop < 0呢? 这个时候就要请出我们的MDN规范老师了
scrollTop 可以被设置为任何整数值,同时注意:
- 如果一个元素不能被滚动(例如,它没有溢出,或者这个元素有一个"non-scrollable"属性), scrollTop将被设置为0。
- 设置scrollTop的值小于0,scrollTop 被设为0
- 如果设置了超出这个容器可滚动的值, scrollTop 会被设为最大值.
测试
it('create', async() => {
vm = createVue({
template: `
<div ref="scrollTarget" class="test-scroll" style="height: 100px; overflow: auto">
<div style="height: 10000px; width: 100%">
<el-backtop target=".test-scroll">
<span>test_up_text</span>
</el-backtop>
</div>
</div>
`
}, true);
expect(vm.$el).to.exist;
expect(vm.$el.innerText).to.be.equal('');
vm.$refs.scrollTarget.scrollTop = 2000;
await wait();
expect(vm.$el.innerText).to.be.equal('test_up_text');
});
测试用例中,对visible进行了测试。这个就比较简单,就无需多讲啦。
与iview的异同
iview的代码
| 不同 | element | iview |
|---|---|---|
| scroll事件 | 节流 | - |
| 滚动对象 | prop(target)决定 | window |
| 显示隐藏 | v-if | display |
| 动画 | setInterval,逐渐加快 | requestAnimationFrame, 匀速 |
动画的实现
提供了一个prop: duration,是滚动动画持续时间,单位毫秒
// scrollTop animation
export function scrollTop(el, from = 0, to, duration = 500, endCallback) {
// `requestAnimationFrame `降级处理处理,代码省略
const difference = Math.abs(from - to);
const step = Math.ceil(difference / duration * 50);
function scroll(start, end, step) {
if (start === end) {
endCallback && endCallback();
return;
}
let d = (start + step > end) ? end : start + step;
if (start > end) {
d = (start - step < end) ? end : start - step;
}
if (el === window) {
window.scrollTo(d, d);
} else {
el.scrollTop = d;
}
window.requestAnimationFrame(() => scroll(d, end, step));
}
scroll(from, to, step);
}
requestAnimationFrame降级处理step = Math.ceil(difference / duration * 50)计算每帧需要滚动的距离
为什么是50?
我也不知道 = = 我觉得是算错了TAT, 应该是*(1000/60)才对
- 设置每一帧的
scrollTop
- 获取滚动的高度方式不同
iview:window.pageYOffset
element-ui:el.scrollTop

总结
结合两个组件库的特点,总结如下:
- 使用
requestAnimationFrame处理动画 - 传递
target,指定触发滚动的对象,我们要的backTop组件并非一定是返回document.documentElement的顶部 - 滚动事件做节流处理