better-scroll 滚动原理

better-scroll它的 html 结构

<div class="wrapper">
  <ul class="content">
    <li>...</li>
    <li>...</li>
    ...  </ul>
  <!-- 这里可以放一些其它的 DOM,但不会影响滚动 --></div>

上面的代码中:

  1. better-scroll 是作用在外层 wrapper 容器上的

  2. 滚动的部分是 content 元素

  3. 这里要注意的是,better-scroll 只处理容器(wrapper)的第一个子元素(content)的滚动其它的元素都会被忽略。

better-scroll 初始化【既获取外层 wrapper 容器,并将 better-scroll 是作用在它上面】:

import BScroll from 'better-scroll'let wrapper = document.querySelector('.wrapper')
let scroll = new BScroll(wrapper,{})
  • better-scroll 提供了一个类,

  • 实例化的第一个参数是一个原生的 DOM 对象。例如上面的document.querySelector('.wrapper');这也说明了 better-scroll 是作用在外层 wrapper 容器上,第二个参数是配置选项

  • 如果传递的是一个字符串,better-scroll 内部会尝试调用 querySelector 去获取这个 DOM 对象,所以初始化代码也可以是这样:
    import BScroll from 'better-scroll'
    let scroll = new BScroll('.wrapper',{})


滚动原理

浏览器的滚动条:
当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也 就是当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容

better-scroll 也是一样的原理,我们可以用一张图更直观的感受一下:

image.png

better-scroll 的滚动原理:

  • 绿色部分为 wrapper,也就是父容器,它会有固定的高度【这一点很重要,很多时候我们在better-scroll 初始化了, 但是没法滚动】

  • 黄色部分为 content,它是父容器的第一个子元素,它的高度会随着内容的大小而撑高。

  • 当 content 的高度不超过父容器的高度,是不能滚动的,而它一旦超过了父容器的高度,我们就可以滚动内容区了

  • better-scroll 的初始化时机很重要,因为它在初始化的时候,会计算父元素和子元素的高度和宽度,来决定是否可以纵向和横向滚动。

  • 我们在初始化它的时候,必须确保父元素和子元素的内容已经正确渲染了

  • 如果子元素或者父元素 DOM 结构发生改变的时候,必须重新调用 scroll.refresh() 方法重新计算来确保滚动效果的正常。

  • 所以 better-scroll 不能滚动的原因多半是初始化 better-scroll 的时机不对,或者是当 DOM 结构发送变化的时候并没有重新计算 better-scroll。

参数配置

better-scroll 支持很多参数配置,可以在初始化的时候传入第二个参数,比如:

let scroll = new BScroll('.wrapper',{

scrollY: true,click: true

})

默认情况:
纵向滚动,横向不滚动。

对工作中配置用到的一些介绍:

probeType:可选值:1、2、3。

有时候我们需要知道滚动的位置

  • 当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;

  • 当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;

  • 当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。

  • 如果没有设置该值,其默认值为 0,即不派发 scroll 事件。

momentum:当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画。设置为 true 则开启动画

better-scroll 遇见 Vue

参考文章:https://zhuanlan.zhihu.com/p/27407024

异步数据的处理

在我们的实际工作中,列表的数据往往都是异步获取的,因此我们初始化 better-scroll 的时机需要在数据获取后,代码如下:

    <template>
      <div class="wrapper" ref="wrapper">
        <ul class="content">
          <li v-for="item in data">{{item}}</li>
        </ul>
      </div>
    </template>
    <script>
      import BScroll from 'better-scroll'
      export default {        data() {          return {            data: []
          }
        },        created() {
          requestData().then((res) => {            this.data = res.data            this.$nextTick(() => {              this.scroll = new Bscroll(this.$refs.wrapper, {})
            })
          })
        }
      }    </script>

在better-scroll滚动原理中我们知道,在初始化better-scroll时必须确保父元素和子元素的内容已经正确渲染了,那么初始化【this.scroll = new Bscroll(this.$refs.wrapper, {})】的时候不是应该在mounted 中吗,但是这里的初始化为什么是在created中呢?

为什么这里在 created 这个钩子函数里请求数据而不是放到 mounted 的钩子函数里?

  • 因为 requestData 是发送一个网络请求,这是一个异步过程,

  • 当拿到响应数据的时候,Vue 的 DOM 早就已经渲染好了,

  • 但是数据改变 —> DOM 重新渲染仍然是一个异步过程,所以即使在我们拿到数据后,也要异步初始化 better-scroll。

数据的动态更新

除了数据异步获取,还有一些场景可以动态更新列表中的数据,比如常见的下拉加载,上拉刷新等,这些动作都有一个共同点,就是数据发生了变化,进而造成了,dom也会跟着变化

better-scroll 配合 Vue 实现下拉加载功能

<template>
  <div class="wrapper" ref="wrapper">
    <ul class="content">
      <li v-for="item in data">{{item}}</li>
    </ul>
    <div class="loading-wrapper"></div>
  </div></template><script>
  import BScroll from 'better-scroll'
  export default {    data() {      return {        data: []
      }
    },    created() {      this.loadData()
    },    methods: {      loadData() {
        requestData().then((res) => {          this.data = res.data.concat(this.data)          this.$nextTick(() => {            if (!this.scroll) {              this.scroll = new Bscroll(this.$refs.wrapper, {})              this.scroll.on('touchend', (pos) => {                // 下拉动作
                if (pos.y > 50) {                  this.loadData()
                }
              })
            } else {              this.scroll.refresh()
            }
          })
        })
      }
    }
  }</script>

上面代码的总结:

  1. 滑动列表松开手指时候【touchend事件:鼠标/手指离开】, better-scroll 会对外派发一个 touchend 事件并对外抛出一个参数pos【{x, y} 位置坐标】;

  2. 监听了这个事件,并且判断了 pos.y > 50(我们把这个行为定义成一次下拉的动作)。如果是下拉的话我们会重新请求数据【this.loadData()】,并且把新的数据和之前的 data 做一次 concat,也就更新了列表的数据,那么数据的改变就会映射到 DOM 的变化。

  3. 重新请求数据就会对this.scroll重新做判断了,如果没有初始化过我们会通过 new BScroll 初始化【例如上面的: this.scroll = new Bscroll(this.$refs.wrapper, {})】,并且绑定一些事件【比如上面的 this.scroll.on('touchend', (pos) => {}】,否则我们会调用 this.scroll.refresh 方法重新计算,来确保滚动效果的正常

scroll 组件的抽象和封装

很多类似滚动的组件,我们就需要写很多类似的命令式且重复性的代码,而且我们把数据请求和 better-scroll 也做了强耦合;但是在Vue项目中我们希望代码之间是低耦合;

croll 组件本质上就是一个可以滚动的列表组件,至于列表的 DOM 结构,只需要满足 better-scroll 的 DOM 结构规范即可,具体用什么标签,有哪些辅助节点(比如下拉刷新上拉加载的 loading 层),这些都不是 scroll 组件需要关心的。因此, scroll 组件的 DOM 结构十分简单,如下所示:

<template>  <div ref="wrapper">
    <slot></slot>
  </div></template> mounted() {  // 保证在DOM渲染完毕后初始化better-scroll
  setTimeout(() => {    this._initScroll()
  }, 20)
},// 是否派发顶部下拉事件,用于下拉刷新
    if (this.pulldown) {      this.scroll.on('touchend', (pos) => {        // 下拉动作
        if (pos.y > 50) {          this.$emit('pulldown',pos)
        }
      })
    }// 监听数据的变化,延时refreshDelay时间后调用refresh方法重新计算,保证滚动效果正常watch: { 
      data() {        setTimeout(() => {          this.refresh()
        }, this.refreshDelay)
      }
    }
  • JS 部分实际上就是对 better-scroll 做一层 Vue 的封装,

  • 通过 props 的形式,把一些对 better-scroll 定制化的控制权交给父组件;

  • 通过 methods 暴露的一些方法对 better-scroll 的方法做一层代理;

  • 通过 watch 传入的 data,当 data 发生改变的时候,在适当的时机调用 refresh 方法重新计算 better-scroll 确保滚动 效果正常,

  • 这里之所以要有一个 refreshDelay 的设置是考虑到如果我们对列表操作用到了 transition-group 做动画效果,那么 DOM 的渲染完毕时间就是在动画完成之后。

利用封装的scroll组件实现上面的下拉加载功能
html代码:

   
   <template>
      <scroll class="wrapper"
              :data="data"
              :pulldown="pulldown"
              @pulldown="loadData">
        <ul class="content">
          <li v-for="item in data">{{item}}</li>
        </ul>
        <div class="loading-wrapper"></div>
      </scroll>
    </template>

js代码:

    
  <script>
  import BScroll from 'better-scroll'
  export default {    data() {      return {        data: [],        pulldown: true
      }
    },    created() {      this.loadData()
    },    methods: {      loadData() {
        requestData().then((res) => {          this.data = res.data.concat(this.data)
        })
      }
    }
  }</script>
  • 可以很明显的看到我们的 JS 部分精简了非常多的代码,

  • 没有对 better-scroll 再做命令式的操作了【这些操作都在srcoll组件中的methods的方法中写好了,我们需要哪些操作只需要调用就好了,比如我们想实现下拉那么我们就用@pulldown="loadData"】,

  • 同时把数据请求和 better-scroll 也做了剥离【在这个页面中我们只想看到起请求数据,自己的数据this.data有更新就好了,其他的交给srcoll组件就好了】

  • 父组件只需要把数据 data 通过 prop 传给 scroll 组件,就可以保证 scroll 组件的滚动效果。

  • 同时,如果想实现下拉刷新的功能,只需要通过 prop 把 pulldown 设置为 true,并且监听 pulldown 的事件去做一些数据获取并更新的动作即可,整个逻辑也是非常清晰的。





















评论者:[[ schemeInfo.user.username ]]

评论内容:[[ schemeInfo.pbody ]]

评论时间:[[ schemeInfo.ptime ]]





发表你的评论:

提交评论
上一篇:
下一篇: