better-scroll的踩坑总结

6,981 阅读3分钟

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时的一些体验与心得,如有不当,欢迎指出。

参考:
当 better-scroll 遇见 Vue