scrollTop的兼容性

scroll事件,当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<body>元素中包含所加载页面的滚动条。

虽然scroll事件是在window对象上发生,但他实际表示的则是页面中相应元素的变化。在混杂模式(document.compatMode的值为BackCompat)下,可以通过<body>元素的scrollLeft和scrollTop来监控到这一变化。

而在标准模式(document.compatMode的值为CSS1Compat)下,除Safari之外的所有浏览器都会通过<html>元素来反应这一变化。

以上内容来自《Javascript 高级程序设计(第三版)》。

以下是我自己测试的结果,截止2017-05-18,用的是最新版的chrome、Firefox和Win7中的IE。

  • 混杂模式下,chrome、IE、Firefox都是通过document.body.scrollTop监听滚动条的位置。
  • 标准模式下,chrome通过document.body.scrollTop监听滚动条位置,IE和Firefox通过document.documentElement.scrollTop监听滚动条位置
  • 可以用下面的代码进行验证:

        function outPutScrollTop() {
            console.log(document.compatMode);
            if(document.compatMode === 'CSS1Compat') {
                console.log(document.documentElement.scrollTop + '标准模式');
                console.log(document.body.scrollTop + '标准模式');
            } else {
                console.log(document.body.scrollTop + '混杂模式');
        // 绑定监听
        window.addEventListener('scroll', outPutScrollTop);

    (去掉文档头部的文档声明就可以开启混杂模式。)

    scroll事件的优化

    scroll事件如果不做优化,默认情况下会频繁地被触发,如果在事件处理程序内进行了复杂的DOM操作,就会大大增加浏览器的负担,消耗性能。

    通过看别人的文章,知道了可以通过防抖函数和节流函数对这种频繁触发的事件进行优化。

    防抖函数达成的效果是:scroll事件被频繁触发时,不会每次都执行事件处理程序中的关键代码,当滚动条停止滚动时,经过事先设置好的时间间隔后才会执行真正想要执行的代码。

    节流函数不像防抖函数那样只在用户停止滚动时才执行事件处理程序中的关键代码,而是在用户滚动滚动条的过程中每隔一定的时间必执行一次事件处理程序中的关键代码。

    以下是对别人文章的总结。

    简单的防抖优化:

        function test() {
            console.log('func');
        window.addEventListener('scroll',function(event) {
                clearTimeout(test.timer);
                test.timer = setTimeout(test,500);
        },false);

    将上面的代码封装成一个函数:

    function debounce(fun,t,immediate) {
        var timeout;
        //返回真正的scroll事件的事件处理程序
        return function(event) {
            var that = this, arg = arguments;
            var later = function() {
                timeout = null;
                if(!immediate) fun.apply(that,arguments);
            var callNow = immediate && !timeout;//这一句位置很重要
            clearTimeout(timeout);
            timeout = setTimeout(later,t);
            if(callNow) {
                fun.apply(that,arguments);
    

    debounce函数接收三个参数:

    第一个参数是一个函数,该函数是事件处理程序中真正想要执行的代码。

    第二个参数是数字,单位毫秒,表示间隔多久调用一次作为第一个参数的函数。这个参数不能小于当前浏览器的最小时间间隔(不同的浏览器的最小时间间隔不同,一般在10~20毫秒,HTML5规范中规定是4毫秒),如果这个参数等于或小于这个最小时间间隔,那么和没有优化没有区别。事实上,未优化时,scroll事件频繁触发的时间间隔也是这个最小时间间隔。

    第三个参数是一个布尔值,不传或为false时,最终的效果与开始那个简单的防抖优化的效果一样;当为true时,表示滚动开始时执行一次作为第一个参数的函数,滚动停止时不执行。

    var myEfficientFn = debounce(function() {
        // 滚动中的真正想要执行的代码
        console.log('ok' + new Date());
    }, 500, false);
    // 绑定监听
    window.addEventListener('scroll', myEfficientFn);

     下面是underscore.js里封装的防抖函数:

    // Returns a function, that, as long as it continues to be invoked, will not
    // be triggered. The function will be called after it stops being called for
    // N milliseconds. If `immediate` is passed, trigger the function on the
    // leading edge, instead of the trailing.
    _.debounce = function(func, wait, immediate) {
      var timeout, args, context, timestamp, result;
      var later = function() {
        var last = _.now() - timestamp;
        if (last < wait && last >= 0) {
          timeout = setTimeout(later, wait - last);
        } else {
          timeout = null;
          if (!immediate) {
            result = func.apply(context, args);
            if (!timeout) context = args = null;
      return function() {
        context = this;
        args = arguments;
        timestamp = _.now();
        var callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
          result = func.apply(context, args);
          context = args = null;
        return result;
    

    简单的节流函数:

    function throttle(fun,t,mustRun,denyLast) {
        var timer = null;
        var startTime = 0;
        return function(event) {
            var that = this, args = arguments;
            clearTimeout(timer);
            var later = function() {
                timer = null;
                if(denyLast) fun.apply(that,args);
                console.log('执行的是later.');
            var currTime = new Date().getTime();
            if(currTime - startTime >= mustRun) {
                console.log(currTime - startTime);
                fun.apply(that,args);
                startTime = currTime;
            } else {
                timer = setTimeout(later,t);
    

    这个节流函数的整体结构与防抖函数的类似,相比防抖函数,节流函数内部多了一个对时间间隔的判断。

    上面这个节流函数接收四个参数:

    第一个参数是一个函数,表示当scroll事件被触发时,开发者真正想要执行的关键代码。

    第二个参数是一个数字,单位毫秒,实际上是要传入setTimeout()方法的第二个参数。(这里setTimeout()的作用就是防止事件处理程序中的关键代码频繁地执行)

    第三个参数也是一个数字,单位毫秒,表示在该时间段内必执行一次关键代码。

    第四个参数是一个布尔值,表示在滚动停止时,是否要执行一次关键代码。true表示执行,false表示不执行。

    在上面的节流函数中,因为startTime是在外部函数中初始化的,所以滚动开始时必会执行一次关键代码。

    节流函数的用法示例:

    var myEfficientFn = throttle(function() {
        // 滚动中的真正想要执行的代码
        console.log('ok' + new Date());
    }, 500,1000,false);
    // 绑定监听
    window.addEventListener('scroll', myEfficientFn);
    //或者这样,效果是一样的
    window.addEventListener('scroll',throttle(function() {
        // 滚动中的真正想要执行的代码
        console.log('ok' + new Date());
    }, 500,1000,false));

     underscore.js里封装的节流函数:

    // Returns a function, that, when invoked, will only be triggered at most once
    // during a given window of time. Normally, the throttled function will run
    // as much as it can, without ever going more than once per `wait` duration;
    // but if you'd like to disable the execution on the leading edge, pass
    // `{leading: false}`. To disable execution on the trailing edge, ditto.
    _.throttle = function(func, wait, options) {
      var context, args, result;
      var timeout = null;
      var previous = 0;
      if (!options) options = {};
      var later = function() {
        previous = options.leading === false ? 0 : _.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      return function() {
        var now = _.now();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
          if (timeout) {
            clearTimeout(timeout);
            timeout = null;
          previous = now;
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
          timeout = setTimeout(later, remaining);
        return result;
    

     上面的防抖函数和节流函数可以应用到所有类似scroll事件这种频繁被触发的事件的优化,比如resize事件、键盘事件、鼠标滚轮事件等。

    参考文章:

    1.【前端性能】高性能滚动 scroll 及页面渲染优化

    2.函数防抖与节流

    3.setTimeout 和 setInterval最小执行时间问题

    4.关于setTimeout()你所不知道的地方