资讯专栏INFORMATION COLUMN

函数除颤/节流提高性能 + 原生实现滚动时到视口时展现

reclay / 2777人阅读

摘要:引言前端开发中一个老生常谈的问题就是当用户滚动时根据滚动的位置适当触发不同的函数动画例如当元素出现在视口时触发该元素的改变通常的做法就是在上附加事件但是我们知道当滚动条滚动时事件触发的是很频繁的且不由控制浏览器的事件队列原生提供如图添加事件

引言

前端开发中一个老生常谈的问题就是"当用户滚动时, 根据滚动的位置适当触发不同的函数/动画, 例如当元素出现在视口时触发该元素的style改变. 通常的做法就是在scrollElement上附加scoll事件. 但是我们知道, 当滚动条滚动时scroll事件触发的是很频繁的, 且不由JS控制(浏览器的事件队列原生提供), 如图(添加事件监听后滚轮三格):

依据系统设置的不同, 一次滚轮触发的scroll事件大概在10~15次之间. 如果在回调函数中添加大量的DOM操作或者计算的话, 会引起明显的卡顿等性能问题. 那有没有办法去稀释回调函数的触发操作呢? 这个时候就需要函数节流(throttle)和debounce(去颤抖)来解决了!

2017-02-06更新函数式版本

这个版本运用闭包封装数据, 修正this指向以加强鲁棒性, 剔除了一开始就显示在视口的元素
talk is cheap, here are the code

// 根据单一元素, throttle函数专门负责事件稀释, 接受两个参数: 要间隔调用的函数, 以及调用间隔. 
var throttle = function (fn, interval) {
  let start = Date.now()
  let first = true
  return function (...args) {
    let now = Date.now()
    // 如果是第一次调用, 则忽略时间间隔
    if (first) {
      fn.apply(this, args)
      first = false
      return
    }
    if (now - start > interval) {
      fn.apply(this, args)
      start = now
    }   
  }
}
// 显示元素的IIFE
var showElems = (function (selector, func) {
  // 预处理, 标识已经显示在视口的元素
  let elemCollect = [...document.querySelectorAll(selector)]
  let innerHeight = window.innerHeight
  let hiddenElems = []
  elemCollect.forEach((elem, index) => {
    let top = elem.getBoundingClientRect().top
    // 不显示在视口才加入判断队列
    if (top > innerHeight) {
      hiddenElems.push(elem)
    }
  })
  // memory release
  elemCollect = null
  return function (...args) {
    hiddenElems.forEach((elem) => {
      let bottom = elem.getBoundingClientRect().bottom
      if (bottom < innerHeight) {
        func.apply(elem, args)
      }
    })
  }
})("p", function(e){
  console.log(this, e, "showed!")
})
// 组合, throttle函数负责稀释showElems触发的频率, showElems负责元素滚动到视口时的相应动作
var throttledScroll = throttle(showElems, 500)
window.addEventListener("scroll", throttledScroll)
debounce

设想一些用户的频繁操作, 例如滚动, 文本框输入等, 每次触发事件都要调用回调函数; 这样做的代价未免大了点. 而debounce的作用就是在在用户高频触发事件时, 暂时不调用回调, 在用户停止高频操作(例如停止输入, 停止滚动时), 再调用一次回调.

解决方案有了, 怎样用代码实现呢? 这里我们要用到setTimeout这个功能来做函数调用的延迟. 具体代码如下(将代码粘贴到console中执行以下, 自己试试看):

            var timer;
            document.addEventListener("scroll", function(){
                clearTimeout(timer); //如果操作时已经有了延迟执行, 则取消该延迟执行
                timer = setTimeout(function() { //设定新的延迟执行
                    callback();
                }, 500)
            })

(这里我们为了方便说明, 设定了timer全局变量. 实践中我们可以将timer附加为函数的属性, 隐藏在闭包中, 或者作为对象的属性等. )

第一次高频操作触发时, 设定一个timer, 在500ms后执行; 如果用户在500ms之内没有再次进行该操作(本例中是滚动), 那么我们调用callback; 然而如果500ms之内用户触发了滚动(即所谓的高频操作), 那么我们清除上一次设定的timeout, 设定一个新的, 500ms之后执行的timeout.

大家思考一下, debounce的本质就是在用户触发expensive操作时, 不断延期该expensive操作的执行时间(取消和设定timeout的代价是很小的). 当用户停止操作, 那我们就不再延期, 最后一次设定的timeout会在500ms后执行expensive operation, 例如dom操作, 计算等.

到这里我们似乎已经有了一个解决方案! 然而还有个小小的问题.....

如果用户不停地操作, 那debounce就会不断把操作延期, 如果用户没有两次操作的间隔时间大于500ms, 那么我们的callback永远也得不到执行. 可怜的callback! 恩, 在这一点上我们当然可以改进...

throttle

throttle的作用是, 保证一个函数至少间隔一段时间得到一次执行. 不像等待用户停止的debounce, throttle即使在用户不停操作时, 也能让callback在操作期间得到间隔的执行.

那么该怎么做呢? 一种方法是在用户开始操作时记录开始时间, 同时设定一个flag ifOperationBegin = true. 之后在每次用户的操作中判断当前时间, 如果当前时间-开始时间 > 某个值, 比如500ms, 则执行callback, 同时设定ifOperationBegin = true, 以开始下一次的设定开始时间 -> 记录操作时间 -> 判断的循环. 具体到代码实现上:

            var scrollBegin = false, scrollStartTime = null; //用户尚未开始操作
            document.addEventListener("scroll", function(){
                if(!scrollBegin)scrollStartTime = Date.now();//记录开始时间, 前提是callback还没有被触发过
                scrollBegin = true;//设定flag
                if(Date.now() - scrollStartTime > 500){ //如果操作时间和开始时间间隔大于500ms则
                    exec(elems, cb); //调用回调
                    scrollBegin = false; //flag设为false, 以设定新的开始时间
                }
            })

这样做的效果是, 在用户持续触发scroll操作时, 保证在用户操作期间callback至少会每隔500ms触发一次. 如果用户操作在500ms之内结束, 那也木有关系, 下一次用户重新开始操作时我们的scrollStartTime 依然保留着, callback会被立即触发.

实际运用

那这两种技术可以运用到哪里呢? 请看如下代码栗子:

        function detectVisible(selector, cb, interval){ //检测元素是否在视口的函数
            var elems = document.querySelectorAll(selector), innerHeight = window.innerHeight;
            var exec = function(elems, cb){ //回调函数
                Array.prototype.forEach.call(elems, function(elem, index){
                    if(elem.getBoundingClientRect().top < innerHeight){ //判断元素是否出现在视口
                        cb.call(elem, elem); //调用传入的回调
                    }
                })
            }
            document.addEventListener("scroll", function(){ //使用debounce和throttle来稀释scroll事件
                clearTimeout(detectVisible.timer);
                if(!detectVisible.scrollBegin)detectVisible.scrollStartTime = Date.now();
                detectVisible.scrollBegin = true;
                if(Date.now() - detectVisible.scrollStartTime > interval){
                    exec(elems, cb);
                    console.log("invoked by throttle!")
                    detectVisible.scrollBegin = false;
                }
                detectVisible.timer = setTimeout(function() {
                    exec(elems, cb);
                    console.log("invoked by debounce!")
                }, interval)
            })
        }
        detectVisible("div.elem", function(elem){
            this.style.backgroundColor = "yellow";
        }, 500);

这个栗子中我们综合运用了throttledebounce, 达到了如下效果: 用户不停滚动时callback会至少每500ms触发一次; 用户停止滚动后的500ms判断函数也会触发一次. 大家可以打开console查看callback何时是被throttle触发的, 何时是被debounce触发的.

总结

这一篇文章的主要主题事件的稀释以期性能上的改善, 有两种解决方法:throttledebounce. 前者是通过在用户操作时判断操作时间, 来达到间隔一段时间触发回调的效果; 而后者则是将触发的时间不断延期, 直到用户停止操作再执行回调. 两者各有优缺点, 两相结合, 我们得到了一个用户无论怎样操作(不停操作或者操作时间极短)都可以保证callback定期得到执行的函数. problem solved!

看完这篇, 如果你有所收获, 请去github给我加个star呗!~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/50227.html

相关文章

  • 函数除颤/节流提高性能 + 原生实现滚动视口展现

    摘要:引言前端开发中一个老生常谈的问题就是当用户滚动时根据滚动的位置适当触发不同的函数动画例如当元素出现在视口时触发该元素的改变通常的做法就是在上附加事件但是我们知道当滚动条滚动时事件触发的是很频繁的且不由控制浏览器的事件队列原生提供如图添加事件 引言 前端开发中一个老生常谈的问题就是当用户滚动时, 根据滚动的位置适当触发不同的函数/动画, 例如当元素出现在视口时触发该元素的style改变....

    mengera88 评论0 收藏0
  • 图片懒加载的前世今生

    摘要:配置项配置项中的参数有以下三个所监听对象的具体祖先元素,默认是计算交叉状态时,将附加到祖先元素上,从而有效的扩大或者缩小祖先元素判定区域设置一系列的阈值,当交叉状态达到阈值时,会触发回调函数。 一、前言   通常情况下,HTML 中的图片资源会自上而下依次加载,而部分图片只有在用户向下滚动页面的场景下才能被看见,否则这部分图片的流量就白白浪费了。   所以,对于那些含有大量图片资源的网...

    zhaot 评论0 收藏0
  • 移动端滚动研究

    摘要:还会有一个性能上的问题就是当页面的列表过长,元素过多时,在模拟滚动,下拉刷新这段时间内,页面也会有卡顿现象,这里采取了一个优化策略即列表较长时数量较多时,在触发下拉刷新的时机时将页面视窗之外的元素隐藏或者存放在里面。 移动web滚动问题 在移动端如果使用局部滚动,意思就是我们的滚动在一个固定宽高的div内触发,将该div设置成overflow:scroll/auto;来形成div内部的...

    ghnor 评论0 收藏0
  • 【前端词典】5 种滚动吸顶实现方式的比较[性能升级版]

    摘要:用于获得当前元素到定位父级顶部的距离偏移值。后来在项目中总会遇到滚动吸顶的效果需要实现,现在我将我知道的种滚动吸顶实现方式做详细介绍。有兼容性问题,在微信浏览器某些版本中的值会为,于是乎也就有了第三种方案的兼容性写法。修改版预览 这篇文章是三天前写就的,有大佬给我提了一些修改意见,我觉得这个意见确实中肯。所以就有了这个升级的修改版本。代码同步更新到 GitHub 了。 修改内容如下: 添加...

    happyfish 评论0 收藏0
  • 延迟加载(Lazyload)三种实现方式

    摘要:原文链接延迟加载也称为惰性加载,即在长网页中延迟加载图像。传入一个回调函数,当其观察到元素集合出现时候,则会执行该函数。管理的是一个数组,当元素出现或消失的时候,数组添加或删除该元素,并且执行该回调函数。 原文链接 - https://zhuanlan.zhihu.com/p/25455672 延迟加载也称为惰性加载,即在长网页中延迟加载图像。用户滚动到它们之前,视口外的图像不会加载。...

    niuxiaowei111 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<