在我们日常的移动端项目开发中处理滚动列表是再常见不过的需求了以滴滴为例可以是这样竖向滚动的列表如图所示
微信 —> 钱包—>滴滴出行”体验效果。
什么是 better-scroll better-scroll 是一个移动端滚动的解决方案它是基于 iscroll 的重写它和 iscroll 的主要区别在 这里 。better-scroll 也很强大不仅可以做普通的滚动列表还可以做轮播图、picker 等等。
不能滚动是现象我们得搞清楚这其中的根本原因。在这之前我们先来看一下浏览器的滚动原理
那么对于 better-scroll 也是一样的道理我们先来看一下 better-scroll 常见的 html 结构
<div class="wrapper"> <ul class="content"> <li>...</li> <li>...</li> ... </ul> </div>
为了更加直观我们再来看一张图
固定的高度 。×××部分为 content它是父容器的第一个子元素它的高度会随着内容的大小而撑高。那么当 content 的高度不超过父容器的高度是不能滚动的而它一旦超过了父容器的高度我们就可以滚动内容区了这就是 better-scroll 的滚动原理。
import BScroll from 'better-scroll' let wrapper = document.querySelector('.wrapper') let scroll = new BScroll(wrapper, {})
better-scroll 的文档 。
scroll.refresh() 方法重新计算来确保滚动效果的正常。所以同学们反馈的 better-scroll 不能滚动的原因 多半是初始化 better-scroll 的时机不对或者是当 DOM 结构发送变化的时候并没有重新计算 better-scroll 。
Vue.js 都不陌生当 better-scroll 遇见 Vue会擦出怎样的火花呢
<template> <div class="wrapper" ref="wrapper"> <ul class="content"> <li>...</li> <li>...</li> ... </ul> </div></template><script> import BScroll from 'better-scroll' export default { mounted() { this.$nextTick(() => { this.scroll = new Bscroll(this.$refs.wrapper, {}) }) } }</script>
js 提供了我们一个获取 DOM 对象的接口—— vm.$refs 。在这里我们通过了 this.$refs.wrapper 访问到了这个 DOM 对象并且我们在 mounted 这个钩子函数里 this.$nextTick 的回调函数中初始化 better-scroll 。因为这个时候wrapper 的 DOM 已经渲染了我们可以正确计算它以及它内层 content 的高度以确保滚动正常。
this.$nextTick 是一个异步函数为了确保 DOM 已经渲染感兴趣的同学可以了解一下它的 内部实现细节 底层用到了 MutationObserver 或者是 setTimeout(fn, 0) 。其实我们在这里把 this.$nextTick 替换成 setTimeout(fn, 20) 也是可以的20 ms 是一个经验值每一个 Tick 约为 17 ms对用户体验而言都是无感知的。
<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>
axios 或者 vue-resource 。我们获取到数据的后需要通过异步的方式再去初始化 better-scroll因为 Vue 是数据驱动的 Vue 数据发生变化 this.data = res.data 到页面重新渲染是一个异步的过程我们的初始化时机是要在 DOM 重新渲染后所以这里用到了 this.$nextTick 当然替换成 setTimeout(fn, 20) 也是可以的。
数据改变 —> DOM 重新渲染仍然是一个异步过程 所以即使在我们拿到数据后也要异步初始化 better-scroll。
<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>
这段代码比之前稍微复杂一些, 当我们在滑动列表松开手指时候 better-scroll 会对外派发一个 touchend 事件我们监听了这个事件并且判断了 pos.y > 50我们把这个行为定义成一次下拉的动作。如果是下拉的话我们会重新请求数据并且把新的数据和之前的 data 做一次 concat也就更新了列表的数据那么数据的改变就会映射到 DOM 的变化。需要注意的一点这里我们对 this.scroll 做了判断如果没有初始化过我们会通过 new BScroll 初始化并且绑定一些事件否则我们会调用 this.scroll.refresh 方法重新计算来确保滚动效果的正常。
scroll 组件的抽象和封装 因此我们有强烈的需求抽象出来一个 scroll 组件类似小程序的 scroll-view 组件方便开发者的使用。
<template> <div ref="wrapper"> <slot></slot> </div> </template>
<script type="text/ecmascript-6"> import BScroll from 'better-scroll' export default { props: { /** * 1 滚动的时候会派发scroll事件会截流。 * 2 滚动的时候实时派发scroll事件不会截流。 * 3 除了实时派发scroll事件在swipe的情况下仍然能实时派发scroll事件 */ probeType: { type: Number, default: 1 }, /** * 点击列表是否派发click事件 */ click: { type: Boolean, default:true }, /** * 是否开启横向滚动 */ scrollX: { type: Boolean, default: false }, /** * 是否派发滚动事件 */ listenScroll: { type:Boolean, default: false }, /** * 列表的数据 */ data: { type: Array, default: null }, /** * 是否派发滚动到底部的事件用于上拉加载 */ pullup: { type: Boolean, default: false }, /** * 是否派发顶部下拉的事件用于下拉刷新 */ pulldown: { type:Boolean, default: false }, /** * 是否派发列表滚动开始的事件 */ beforeScroll: { type: Boolean, default: false }, /** * 当数据更新后刷新scroll的延时。 */ refreshDelay: { type: Number, default: 20 } }, mounted() { // 保证在DOM渲染完毕后初始化better-scroll setTimeout(() => { this._initScroll() }, 20) }, methods: { _initScroll() { if (!this.$refs.wrapper) { return }// better-scroll的初始化 this.scroll = new BScroll(this.$refs.wrapper, { probeType: this.probeType, click: this.click, scrollX: this.scrollX }) // 是否派发滚动事件 if (this.listenScroll) { let me = this this.scroll.on('scroll', (pos) => { me.$emit('scroll', pos) }) } // 是否派发滚动到底部事件用于上拉加载 if (this.pullup) { this.scroll.on('scrollEnd', () => { // 滚动到底部 if (this.scroll.y <= (this.scroll.maxScrollY + 50)) { this.$emit('scrollToEnd') } }) } // 是否派发顶部下拉事件用于下拉刷新 if (this.pulldown) { this.scroll.on('touchend', (pos) => { // 下拉动作 if (pos.y > 50) { this.$emit('pulldown') } }) } // 是否派发列表滚动开始的事件 if (this.beforeScroll) { this.scroll.on('beforeScrollStart', () => {this.$emit('beforeScroll') }) } }, disable() { // 代理better-scroll的disable方法 this.scroll && this.scroll.disable() }, enable() { // 代理better-scroll的enable方法 this.scroll && this.scroll.enable() }, refresh() { // 代理better-scroll的refresh方法this.scroll && this.scroll.refresh() }, scrollTo() { // 代理better-scroll的scrollTo方法 this.scroll &&this.scroll.scrollTo.apply(this.scroll, arguments) }, scrollToElement() { // 代理better-scroll的scrollToElement方法this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) } }, watch: { // 监听数据的变化延时refreshDelay时间后调用refresh方法重新计算保证滚动效果正常 data() { setTimeout(() => { this.refresh() }, this.refreshDelay) } } } </script>
有了这一层 scroll 组件的封装我们来修改刚刚最复杂的代码假设我们已经全局注册了 scroll 组件。
<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><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>
插件 Vue 化引发的一些思考 这篇文章我不仅仅是要教会大家封装一个 scroll 组件还想传递一些把第三方插件原生 JS 实现Vue 化的思考过程。很多学习 Vue.js 的同学可能还停留在 “XX 效果如何用 Vue.js 实现” 的程度其实把插件 Vue 化有两点很关键一个是对插件本身的实现原理很了解另一个是对 Vue.js 的特性很了解。对插件本身的实现原理了解需要的是一个思考和钻研的过程这个过程可能困难但是收获也是巨大的而对 Vue.js 的特性的了解是需要大家对 Vue.js 多多使用学会从平时的项目中积累和总结也要善于查阅 Vue.js 的官方文档关注一些 Vue.js 的升级等。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。