better-scroll是什么
BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。
vue中better-scroll的简单使用
<div class="wrapper">
<ul class="content">
<li>...</li>
<li ref='child'>...</li>
...
</ul>
<!-- 这里可以放一些其它的 DOM,但不会影响滚动 -->
</div>
使用better-scroll,首先一定要有三层的html元素,最外层是wrapper,要有固定的高度,而且要设置 overflow: hidden
样式。
为什么需要中间的一层content,而不直接用wrapper包含所有<li>标签呢?
因为better-scroll默认处理第一个子元素的滚动,而忽略其他子元素,因此我们需要content这一层来包裹真正滚动的元素。
使用之前,我们要先通过npm install一下better-scroll,然后再import进来:
import Bscroll from 'better-scroll'
export default {
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper, {
click: true
})
}
}
通过this.$ref取到wrapper这个dom进行初始化,构造函数的第二个是参数是配置项。由于我们使用better-scroll滚动元素默认是不可点击的,因此可以通过设置click为true派发点击事件。
另外,better-scroll也提供一个方法scrollToElement,通过这个方法,可以将页面滚动到指定的子元素:
this.scrollToElement(this.$ref.child)
踩坑总结
初始化Bscroll之后页面无法滚动
首先,我们要知道better-scroll的滚动原理,如下图,只有当content的高度大于wrapper的高度,页面才能够滚动。
但在项目开发过程中,我发现,我的content的高度确实大于wrapper的高度时,页面依然无法滚动,这是为什么?其实原因很有可能是new scroll的时机不对,导致初始化时,wrapper的高度大于content。而我们看到的content高度大于wrapper,其实是在bscroll初始化结束后的因为获取到数据或其他方式改变的。因此,我们初始化Bscroll的时机很重要。
在很多实际应用中,我们列表的数据往往是动态获取的。而由数据改变到触发页面重新渲染又是一个异步的过程,我们要在获取到数据,并且页面已经重新渲染过后,再进行Bscroll的初始化。
官方文档描述:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。 因此,我们可以在数据变化后立即使用nextTick函数,在下一个事件循环中,即dom更新后,再new Bsroll。
created () {
requestData().then((res) => {
this.data = res.data
this.$nextTick(() => {
this.scroll = new Bscroll(this.$refs.wrapper)
})
})
}
scroll.refresh()
由于数据是可能不断变化的,我们不可能每次数据变更都初始化一个Bscroll,这时,我们可以使用better-scroll提供的另一个方法refresh(),触发重新计算wrapper和content的高度,比如在updated钩子函数或监听某个数据的函数中使用。
//父组件部分代码
<template>
<div>
...
<city-list :cities="cities"></city-list>
...
</div>
</template>
export default {
name: 'City',
data () {
return {
cities: {}
}
}
},
methods: {
getCityInfo () {
axios.get('/api/city.json').then(this.getCityInfoSucc)
},
getCityInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.cities = data.cities
this.hotCities = data.hotCities
}
},
},
created () {
this.getCityInfo()
}
}
//子组件部分代码
<div class="list" ref="wrapper">
<div>
...
<div class="area" v-for="(item, key) of cities" :key="key" :ref="key">
<div class="title border-topbottom">{{key}}</div>
<div class="item-list">
<div
class="item border-bottom"
v-for="innerItem of item"
:key="innerItem.id"
@click="handleCityClick(innerItem.name)"
>
{{innerItem.name}}
</div>
</div>
</div>
</div>
</div>
export default {
name: 'CityList',
props: {
cities: Object
},
methods: {
mounted () {
this.scroll = new Bscroll(this.$refs.wrapper, {
click: true
})
},
updated () {
this.scroll.refresh()
}
}
上面是我开发项目中的部分代码,其中,父组件在created后通过axios请求数据,以props的形式传值给子组件,子组件要根据这个值渲染列表,也就是说,子组件里用到了Bscroll,但数据和dom结构依赖于父组件传过来的值而动态更新。
在这里,我没法在数据获取成功的回调函数中使用nextTick,因此我想到一个办法,在updated钩子函数中,使用refresh函数,当父组件获取数据成功后,props中的值也会相应update,此时让scroll重新计算dom元素的高度,列表便能正常滚动了。
当然,如果担心数据频繁变化,导致refresh刷新频繁,影响性能的话,可以利用timer进行防抖操作。
//data里定义一个timer
updated () {
const refresh=this.debounce(500)
refresh()
},
methods: {
debounce(delay){
let timer1 = null
return () => {
if (timer1) clearTimeout(timer1)
timer1 = setTimeout(() => {
this.scroll.refresh()
}, delay)
}
}
better-scroll和scrollBehavior
在项目中,我想让每次进入这个页面时,列表都回到顶端,因此我利用vue-router的scrollBehavior:
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
然而这在better-scroll列表页中并没有没有生效,但是重新进入其他页面都能回到顶端,这又是为什么呢?
我们要搞清楚这两个scroll的原理。当使用vue-router时,scrollBehavior记录的是整个页面的滚动,也就是说,页面是由于内容太多,自动撑高的,这个高度不固定。而使用better-scroll(再次引用上面那张图),外层wrapper的高度是固定下来的,滚动的是wrapper内的元素,因此,整个页面其实是没有滚动的,这样就造成了列表里面的元素还停留在原来的位置,但页面其实已经回到顶端的现象。
那要怎么让better-scroll内滚动的元素回到最顶端呢?
可以利用scrollToElement(this.$refs.wrapper)方法,或者scrollTo(0, 0)方法。
以上是我自己在使用better-scroll时的一些体验与心得,如有不当,欢迎指出。