最优-scroll事件的监听实现分分快三计划

作者:分分快三计划

2. window.requestAnimationFrame()

前文说到,如果利用setTimeout或者setInterval,回调间隔interval很难确定。最理想的情况就是:回调间隔等于显示屏(浏览器)刷新频率。

浏览器刷新频率一般会略低于显示屏刷新频率,为16.7次/ms。具体说,就是:scroll事件每次触发时候的时间间隔。通过代码来看一下:

var app = document.getElementById("app");
app.addEventListener("scroll", function() {
  console.log(new Date().getTime());
});

控制台输出:
分分快三计划 1

可以看到,有时候间隔是 10ms,有时候是 30ms,如果我们自己来设定interval,应该取最小值。然而,不同浏览器和不同电脑的刷新频率不确定。如果设置过小,还会造成刷新频率低的显示屏的 CPU 损耗。

所以,使用window.requestAnimationFrame()来让浏览器根据刷新频率自动设置interval。我们只需要关注回调函数即可。

当然,这个函数本身还实现了很多优化,可以点我看一下。

为什么?

浏览器中某些计算和处理比其他要昂贵的多。例如,DOM操作比非DOM交互需要更多的内存和CPU时间。连续尝试很多DOM操作会导致浏览器挂起甚至崩溃。比如:重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动(mousemove)。也就是说用户在触发这些浏览器操作的时候,如果脚本里面绑定了对应的事件处理方法,这个方法就不停的触发。

debounce 应用场景

函数去抖有哪些应用场景?哪些时候对于连续的事件响应我们只需要执行一次回调?

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

4. 代码封装

函数封装详见script.js,调用样例详见index.html

基于上面,我们封装script.js

// 节流函数 : 减少浏览器内存消耗
function throttle(ele, callback) {
  var isRunning = false;
  return function() {
    if (isRunning) return;
    isRunning = true;
    // requestAnimationFrame:回调间隔 = 浏览器重绘频率
    window.requestAnimationFrame(function(timestamp) {
      if (ele.scrollTop   ele.clientHeight >= ele.scrollHeight) {
        // 检测是否滚动到元素底部
        callback();
      }
      isRunning = false;
    });
  };
}

/**
 * 监听HTML元素是否滚动到底部 : 兼容ES5
 * @param {object} ele HTML元素
 * @param {function} callback 滚动到底部后的回调函数
 */
function listenScrollToBottom(ele, callback) {
  if (ele === null || ele === undefined) {
    // 节点不存在:抛出错误
    throw new Error("Undefined COM");
    return;
  }
  ele.addEventListener("scroll", throttle(ele, callback), false); // 监听 scroll 事件
}

将需要监听的 HTML 元素和回调函数传入,即可在 HTML 元素滚动到底部时,触发相应的操作。例如:瀑布流、缓冲加载等。下面是控制台输出一段文字。

<body>
  <div id="app">
    <div class="inner"></div>
  </div>
  <script>
    var app = document.getElementById('app')
    listenScrollToBottom(app , function() { // 回调函数
      console.log("Scroll to bottom")
    })
  </script>
</body>

是什么?

就是让一个函数无法在很短的时间间隔内连续执行,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。

概述

也是好久没更新 源码解读,看着房价蹭蹭暴涨,心里也是五味杂陈,对未来充满恐惧和迷茫 ...(敢问一句你们上岸了吗)

言归正传,今天要介绍的是 underscore 中两个重要的方法,函数节流和函数去抖。这篇文章不会涉及具体的代码实现(关于代码实现请期待下文),会从零开始介绍函数节流和函数去抖的概念,辨析应用场景。为什么我对这两个方法情有独钟要花大篇幅去介绍?因为就是它们带我入了「underscore 源码解读」的坑(详见 一次发现underscore源码bug的经历以及对学术界『拿来主义』的思考)。

函数节流和去抖的出现场景,一般都伴随着客户端 DOM 的事件监听。举个例子,实现一个原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置 dom 的位置(样式改变)。如果我们不加以控制,每移动一定像素而触发的回调数量是会非常惊人的,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死,这样的用户体验是非常糟糕的。我们需要做的是降低触发回调的频率,比如让它 500ms 触发一次,或者 200ms,甚至 100ms,这个阈值不能太大,太大了拖拽就会失真,也不能太小,太小了低版本浏览器可能就会假死,这样的解决方案就是函数节流,英文名字叫「throttle」。函数节流的核心是,让一个函数不要执行得太频繁,减少一些过快的调用来节流。

说完函数节流,再看它的好基友函数去抖(debounce)。思考这样一个场景,对于浏览器窗口,每做一次 resize 操作,发送一个请求,很显然,我们需要监听 resize 事件,但是和 mousemove 一样,每缩小(或者放大)一次浏览器,实际上会触发 N 多次的 resize 事件,用节流?节流只能保证定时触发,我们一次就好,这就要用去抖。简单的说,函数去抖就是对于一定时间段的连续的函数调用,只让其执行一次。

3. 节流函数

由于window.requestAnimationFrame()的特效,所以它可以在同一帧中被重复绘制。这时候,就需要节流函数,保证requestAnimationFrame的回调队列中只有一个函数在执行

// 节流函数 : 减少浏览器内存消耗
function throttle(ele, callback) {
  var isRunning = false;
  return function() {
    if (isRunning) return;
    isRunning = true;
    // requestAnimationFrame:回调间隔 = 浏览器重绘频率
    window.requestAnimationFrame(function(timestamp) {
      if (ele.scrollTop   ele.clientHeight >= ele.scrollHeight) {
        // 检测是否滚动到元素底部
        callback();
      }
      isRunning = false;
    });
  };
}

throttle 应用场景

函数节流有哪些应用场景?哪些时候我们需要间隔一定时间触发回调来控制函数调用频率?

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次 https://github.com/hanzichi/underscore-analysis/issues/21#issuecomment-252802650

4. 关于

欢迎技术交流,引用请注明出处。
个人网站:董沅鑫的个人网站
原文链接:最优-scroll事件的监听实现

前端菜鸟一只,查阅了一些资料,大概明白了什么是函数节流

小结

函数节流和函数去抖的核心其实就是限制某一个方法被频繁触发,而一个方法之所以会被频繁触发,大多数情况下是因为 DOM 事件的监听回调,而这也是函数节流以及去抖多数情况下的应用场景。至于函数节流和去抖方法的具体代码实现以及调用方式,下文我们再做分享。

附另外两篇关于 underscore 函数节流以及去抖源码剖析的文章

  • underscore 函数去抖的实现
  • underscore 函数节流的实现

1. 背景和目标

前端在监听scroll这类高频率触发事件时,常常需要一个监听函数来实现监听和回调处理。传统写法上利用setIntervalsetTimeout来实现。

为了减小 CPU 开支,往往需要节流函数,但是,interval的指定依旧是个难题。interval较大,会处理不及时;较小,占用内存资源。

为了实践和解决问题,打算实现一个监听 HTML 元素滚动到底部的函数

  1. 监听指定 HTML 元素的scroll事件,并正确判断是否到底部
  2. 正确确定确定回调间隔
  3. 正确使用节流函数
  4. 组件封装

应用场景

如果函数节流根据应用场景具体细分,还分两种。

  • 就是对于一定时间段的连续的函数调用,只让其执行一次回调函数。就是网上说的函数防抖(debounce)。

    • 每次 resize/scroll 触发统计事件
    • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)
  • 让一个函数不要执行得太频繁,减少一些过快的调用来节流。我们需要间隔一定时间触发回调来控制函数调用频率就是网上说的函数节流(throttle)。

    • DOM 元素的拖拽功能实现(mousemove)
      射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
    • 计算鼠标移动的距离(mousemove)
    • Canvas 模拟画板功能(mousemove)
    • 搜索联想(keyup)
    • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次。

简单实现

  var timer;
  function throttle(){
    if(timer){
      clearTimeout(timer);
      console.log('3');
    }
    timer=setTimeout(function(){
      console.log('函数防抖')
    },1000);
  }
  for(var i=0;i<10;i  ){
  console.log("1");
  throttle();
  console.log('2');
}

分分快三计划 2

上面的代码其实就是一个函数防抖,只执行最后一次。

  • 首先输出1,然后第一次调用函数,timer为undefined,所以不执行if语句,创建一个定时器,1秒后输出‘函数防抖’,timer有值,输出2,-
  • 但是马上又循环了一遍,输出1,这回timer有值,但是清除了原先的定时器,输出3,所以计时要重来,又创建一个,1秒后输出‘函数防抖’,timer有值,输出2
  • 接下来几步和第二步相同,到最后一次throttle()函数调用的时候,没有再清除timer定时器的机会了,所以一秒后输出‘函数防抖’。

接下来我们对整体进行封装

  function throttle(fn, delay) {
      var timer = null;
      return function () {
        clearTimeout(timer);
        timer = setTimeout(function () {
          fn(arguments)}, delay);
      }
    }

    function fn() {
      console.log("函数防抖");
    }
    var fn2 = throttle(fn, 1000);
    fn2();
    fn2();
    fn2();

然后我们再次改进变成函数节流,可以隔断时间触发回调函数

var throttle = function (fn, delay, atleast) {
    var timer = null;
    var previous = null;

    return function () {
        var now =  new Date();

        if ( !previous ) previous = now;

        if ( now - previous > atleast ) {
            fn();
            // 重置上一次开始时间为本次结束时间
            previous = now;
        } else {
            clearTimeout(timer);
            timer = setTimeout(function() {
                fn();
            }, delay);
        }
    }
};

参考资料:函数节流详解

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 前端学习笔记