使用Vue观察者实现一个简单异步无限滚动效果

发布于 大漠

无限滚动(Infinite Scroll)是一种很常见的用户体验模式,它建议用户在Web页面或应用程序加载显示很少的内容。当用户开始向下滚动页面时,会加载更多内容。这些内容是通过向负责提供内容的服务器发出请求来异步加载的。在这篇文章中,我将讨论JavaScript的异步操作以及Vue如何实现无限滚动效果。在这个过程中,我们将看到一个使用无限增发动的简单页面。

理解异步操作

在程序中编写一段同步代码,比如下面的例子,有两行代码:L1L2。如果L1未结速,L2是不会执行的:

console.log('quavo');
console.log('takeoff');

使用Vue观察实现一个简单异步无限滚动效果

通常情况下,我们会看到上面的代码执行的顺序,那是因为$http.get请求需要一些时间才能从our_url获取data,JavaScript并不会花时间去等,而是在等待$http.get时执行下一行代码。完成它所做的事情,这样就可以在控制台上进行日志记录。异步写代码的方法还包括:

setTimeout()函数,它可以先执行后面的一些事情,然后再执行setTimeout()中的代码。比如:

console.log('我先执行')
setTimeout(() => {
    console.log('是的,我等待3s后才执行')
},3000)
console.log('对了,我会第二个执行')

使用Vue观察实现一个简单异步无限滚动效果

高阶函数(也称为回调函数)是作为参数传递给其他函数的函数。让我们假设有一个名为function X的回调函数,它被当做一个参数传递给另一个函数function Y。最终,函数X被执行或调用内部Y函数。比如下面的代码:

function Y(X) {
    $.get("our_url", X);
}

来看一个实例:

var add = function (a, b) {
    return a + b;
}

function math (func, array) {
    return func(array[0] , array[1]);
}

console.log(math(add, [1, 2])) // => 3

上面的例子中传进去的add是一个参数,而在return的时候刚是一个函数。

高阶函数存在于不同的模式中,很有可能你在不知道的情况下就使用它们。比如window.onclicksetTimeout()setInterval()。除此之外,还有一些常见的高阶函数例子。比如jQuery ajax回调函数:

$.ajax({
    url: '//localhost:8000/api/v1/entry/1',
    type: 'GET',
    dataType: 'json',
    success: function (data) {
        // success 接收回调函数
        console.log(data)
    }
})

setTimeout()setInterval()这样的计时器函数也是高阶函数:

setTimeout(function (){
    console.log('是的,我会在3s后执行')
},3000)

var i = 0;
setInterval(function (){
    i++;
    console.log(`我现在的值是:${i}`)
}, 100)

使用Vue观察实现一个简单异步无限滚动效果

在数组中的sort()map()reduce()filter()等函数都是高阶函数的示例:

var arr = [2, 8, 20, 5, 17, 38, 21];

// 升序
arr.sort(function (x, y) {
    return x - y
})

// 降序
arr.sort(function (x, y) {
    return y - x
})

// 数据所有元素进行求平方得到新数组
arr.map(function (item) {
    return Math.pow(item, 2)
})

// 数组求和
arr.reduce(function (x, y) {
    return x + y
})

// 过滤掉数组中的偶数,只留奇数,返回一个新数组

arr.filter(function (x) {
    return x % 2 !== 0
})

使用Vue观察实现一个简单异步无限滚动效果

有关于高阶函数更多介绍,可以阅读下面的内容:

什么是观察者

Vue观察者允许我们在更改数据时执行异步操作。它们就像是在Vue实例中对数据做更改,而视图做出相应的反应。在我们的Vue实例中,观察者用watch关键词表示,并因此被使用:

let app = new Vue({
    el: '#app',
    data () {
        return {
            // 和view绑定的数据
        }
    },
    watch: {
        // 将在app中使用的异步操作
    }
})

扩展观察者的异步操作

让我们看看Vue如何使用观察者来监视异步操作。使用Vue构建一个具有无限滚动特性的应用程序,一旦用户到达页面的底部,就会执行GET请求,并检索更多的数据。这篇文章的示例是来自于@Sarah Drasner的原始案例(我是她的超级粉丝)。接下来看它是如何工作的。

首先使用<script>标签导入必要的库。在这里,我们将导航Vue和Axios,这是一个基于HTTP客户端的浏览器。

<head>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

如果你在Codepen上写的话,可以直接在JavaScript设置中导入相关的脚本,如下图所示:

使用Vue观察实现一个简单异步无限滚动效果

创建一个新的Vue实例:

let app = new Vue({
    // el属性是一个挂载器,指向index.html中的#app的DOM元素
    el: '#app',
})

创建data()函数,并附加需要绑定到DOM的数据属性。最初我们是不会在页面的底部,因此bottom的值设置为false。另外设置beers属性,先设置为一个空数组。

let app = new Vue({
    el: '#app',
    data () {
        return {
            bottom: false,
            beers: []
        }
    }
})

接下来使用methods属性创建所需要的方法。methods允许我们创建函数,并将事件绑定到这些函数以及处理相关的事件。在bottomVisible()函数中,我们使用三个只读属性手动创建无限滚动相关的特性:

  • scrollY:返回滚动条距离viewport顶部边缘的Y坐标。如果没有离开viewport,返回的值为0
  • clientHeight:无素可视区高度,包括padding,但不包括水平滚动条高度、bordermargin
  • scrollHeight:元素内容的高度,包括由于溢出在屏幕上不可见的内容

有关于这方面的相关属性的介绍,可以阅读前段时间整理的一篇笔记《视口宽高、位置与滚动高度》。

let app = new Vue({
    el: '#app',
    data () {
        return {
            bottom: false,
            beers: []
        }
    },
    methods: {
        bottomVisible () {
            const scrollY = window.scrollY
            const visible = document.documentElement.clientHeight
            const pageHeight = document.documentElement.scrollHeight
            const bottomOfPage = visible + scrollY >= pageHeight
            
            return bottomOfPage || pageHeight < visible
        }
    }
})

methods中继续添加另一个函数addBeer(),我们使用Axios执行GET请求。使用Promisescallback,创建一个apiInfo对象,并从API中调用检索值传给它。我们的Vue实例中的每个函数都可以使用this访问data属性。

let app = new Vue({
    el: '#app',
    data () {
        return {
            bottom: false,
            beers: []
        }
    },
    methods: {
        bottomVisible () {
            const scrollY = window.scrollY
            const visible = document.documentElement.clientHeight
            const pageHeight = document.documentElement.scrollHeight
            const bottomOfPage = visible + scrollY >= pageHeight
            
            return bottomOfPage || pageHeight < visible
        },
        addBeer () {
            axios.get('https://api.punkapi.com/v2/beers/random')
            .then(response => {
                let api = response.data[0]
                let apiInfo = {
                    name: api.name,
                    desc: api.description,
                    img: api.image_url,
                    tips: api.brewers_tips,
                    tagline: api.tagline,
                    food: api.food_pairing
                }
                this.beers.push(apiInfo)
                if (this.bottomVisible()) {
                    this.addBeer()
                }
            })
        }
    }
})

有关于Vue中的methods相关的知识可以阅读前段时间整理的相关学习笔记《Vue的Methods》、《Vue的Methods和事件处理》和《在Vue中何时使用方法、计算属性或观察者》。

watch属性中添加相应的观察者,用来监视应用程序状态的变化,并相应的更新DOM:

let app = new Vue({
    el: '#app',
    data () {
        return {
            bottom: false,
            beers: []
        }
    },
    methods: {
        bottomVisible () {
            const scrollY = window.scrollY
            const visible = document.documentElement.clientHeight
            const pageHeight = document.documentElement.scrollHeight
            const bottomOfPage = visible + scrollY >= pageHeight
            
            return bottomOfPage || pageHeight < visible
        },
        addBeer () {
            axios.get('https://api.punkapi.com/v2/beers/random')
            .then(response => {
                let api = response.data[0]
                let apiInfo = {
                    name: api.name,
                    desc: api.description,
                    img: api.image_url,
                    tips: api.brewers_tips,
                    tagline: api.tagline,
                    food: api.food_pairing
                }
                this.beers.push(apiInfo)
                if (this.bottomVisible()) {
                    this.addBeer()
                }
            })
        }
    },
    watch: {
        bottom (bottom) {
            if (bottom) {
                this.addBeer()
            }
        }
    }
})

有关于Vue中的观察者相关的知识,可以阅读前段时间整理的学习笔记:《Vue的观察者》。

接下来再使用Vue的生命周期的钩子created添加一个scroll滚动事件,每次调用bottomVisible()函数时触发一个滚动事件。为了实现无限滚动特性,将data函数中的bottom值设置为bottomVisible()函数。created钩子允许我们访问反应性数据和Vue实例中的函数。

let app = new Vue({
    el: '#app',
    data () {
        return {
            bottom: false,
            beers: []
        }
    },
    methods: {
        bottomVisible () {
            const scrollY = window.scrollY
            const visible = document.documentElement.clientHeight
            const pageHeight = document.documentElement.scrollHeight
            const bottomOfPage = visible + scrollY >= pageHeight
            
            return bottomOfPage || pageHeight < visible
        },
        addBeer () {
            axios.get('https://api.punkapi.com/v2/beers/random')
            .then(response => {
                let api = response.data[0]
                let apiInfo = {
                    name: api.name,
                    desc: api.description,
                    img: api.image_url,
                    tips: api.brewers_tips,
                    tagline: api.tagline,
                    food: api.food_pairing
                }
                this.beers.push(apiInfo)
                if (this.bottomVisible()) {
                    this.addBeer()
                }
            })
        }
    },
    watch: {
        bottom (bottom) {
            if (bottom) {
                this.addBeer()
            }
        }
    },
    created () {
        window.addEventListener('scroll', () => {
            this.bottom = this.bottomVisible()
        })
        this.addBeer()
    }
})

有关于Vue实例和生命周期相关的知识可以阅读前段时间整理的学习笔记《Vue实例和生命周期》。

现在把注意力集中到DOM中,将使用Vue指令让DOM和Vue实例之间实现数据的双向绑定:

  • v-if:根据条件的布尔值,有条件的渲染DOM元素
  • v-for:基于数组循环遍历出项目列表,比如beer in beers,其中beer是被迭代的数组元素的别名

有关于Vue的指令相关的介绍,可以阅读:

示例中的DOM这样写:

<div id="app">
    <section>
        <h1>Make yourself some Punk Beers</h1>
        <!-- beers数组值为空时,显示正在加载中的状态 -->
        <div class="beer-container">
        <div v-if="beers.length === 0" class="loading">Loading...</div>
        <!-- 对beers数组进行迭代 -->
        <div v-for="beer in beers" class="beer-contain">
            <div class="beer-img">
                <img :src="beer.img" height="350" />
            </div>
            <div class="beer-info">
                <h2>{{ beer.name }}</h2>
                <div class="beer-description">
                    <p><span class="bright">Description:</span> {{ beer.desc }}</p>
                </div>
            </div>
        </div>
        </div>
    </section>
</div>

这个时候你的页面可以看到这样的结果:

使用Vue观察实现一个简单异步无限滚动效果

因为还没有添加任何样式,看上去丑丑的。如果添加了样式之后,就可以看到下面这样的效果:

注意,我在@Sarah Drasner的示例上删除了一些字段,只是为了样式上看上去好看一点。原始示例的效果和源码,点击这里可以看到

这个时候你滚动到页面的底部的时候就可以无限加载内容。这个效果也就是我们所说的无限滚动效果:

使用Vue观察实现一个简单异步无限滚动效果

总结

有人可能会问,为什么我们不直接使用Vue的computed属性呢?原因是computed属性是同步的,必须返回一个值。当执行类似timeout函数之类的异步操作,或者像上面示例中GET请求时,最好使用Vue的watch,因为Vue会侦听函数的返回值。使用事件监听器也很酷,但这些都有手工处理事件和调用方法而不是只监听数据更改的缺点。无论如何,当你看到或想要在Vue中使用它们时,你现在知道如何处理异步操作了吧。

特别声明,本文整个思路是跟着@Chris Nwamba的博文《Simple Asynchronous Infinite Scroll with Vue Watchers》学习整理的。文章有关于Vue的示例代码,都源于此文。

由说作者是Vue相关的初学者,如果文章中有不对之处,还请各路大婶拍正。如果你有更好的建议或者想法,欢迎在下面的评论中与我们一起分享。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/vue/simple-asynchronous-infinite-scroll-with-vue-watchers.htmlNike Huaraches Are Hyped