函数节流的分析以及实际应用场景

381 阅读6分钟

函数节流

  JavaScript中的函数多数是由用户触发的,在一些情况下函数的触发不受用户的直接控制,比如在某些场景下函数会被频繁的调用,从而造成性能上的影响。

函数频繁调用的场景

  • window.onresize 事件。   window.onresize事件是在浏览器窗口大小发生改变时触发,当用户在拖拽改变浏览器窗口大小时,在此过程中会频繁调用执行。
  • window.scroll 事件。   window.scroll 事件在浏览器滚动条滚动时触发,在滚动页面的过程中会频繁调用执行。

  以上都是函数被频繁调用的场景,而且在实际的项目开发中也是比较常见的情况。不论是改变窗口大小还是页面滚动,都面临着函数调用频率高的问题,如何控制频率就要用到函数节流。

函数节流分析

  如何实现函数节流,以最常用的滚动事件为例,每次触发滚动时间就打印出当前滚动条的位置。当滚动一次是会发现打印了多条。其实在实际的项目需求中可能是需要一次或两次即可,在这个过程中的其他打印就要被忽略。比如按照时间进行忽略,在某一个时间段内只打印一次。

    // 常规方法,滚动打印
    window.onscroll = function () {
        console.info('window.scrollY:' + window.scrollY);
    }

函数节流的实现

  关于函数节流方法的实现有多种,常见的是采用setTimeout函数延迟执行,延迟时间可以根据需要进行设置。如果在延迟的时间段内则忽略下次调用函数的请求。

    // 使用时间控制,在一段时间内仅打印一次
    window.onscroll = (function () {
        var t = null,
            _self = window;
        return function () {
            if (!t) { // 如果在运行期间,则跳过
                t = setTimeout(function () {
                    clearTimeout(t);
                    t = null;
                    console.info('window.scrollY:' + _self.scrollY);
                }, 500); // 这里的时间可以接收变量参数
            }
        }
    })();

  以不变应万变,将共同的部分提取出来,可以进行函数的封装处理,将普通函数处理为节流函数。

    // 把函数部分提取出来,将节流函数封装为统一模板
    var throttle = function (fn, interval = 500) {
        var t = null,
            _self = this;
        return function (...args) {
            if (!t) {
                t = setTimeout(function () {
                    clearTimeout(t);
                    t = null;
                    fn.apply(_self, args);
                }, interval);
            }
        }
    }
    window.onscroll = throttle(() => {
        console.info('window.scrollY:' + window.scrollY);
    });

  还有一中方法是借助于时间戳,可以进行尝试一下,如果当前时间节点小于最后一次函数执行的时间节点和时间间隔的和,则跳过,大于则执行。

    // 保存最后一次执行函数的时间戳
    var lastTime= 0;
    // 函数执行时获取时间戳。
    var nowTime = Date.now() || new Date().getTime();

    // 判断现在的时间是否大于最后一次执行时间加上时间间隔
    // 不满足直接跳过
    if(newTime > lastTime+interval){
        // 执行函数
        // 设置最后执行时间戳
        lastTime = newTime;
    }

批次处理

  在实际项目中还有这样的一个需求,在页面中列出所有省市县的名字,按照地区分类共用户选择。如果一个地区名称是一个节点,当渲染页面时,一次性渲染所有的节点,会出现同时创建成千上百个节点的情况,短时间内创建并插入大量的接到到页面当中,会造成浏览器的超负荷运载,甚至出现浏览器的卡顿以及假死。

    // 常规加载方式
    var cityData = []; // 城市数据,有n多个
    // 模拟数据
    for(var i=0;i<1000;i++){
        cityData.push(i);
    }

    // 创建节点,将节点插入到页面当中
    var inset = function(data){
        for(var i=0,l=data.length;i<l;i++){
            // 创建节点
            var div = document.createElement('div');
            div.innerHTML = data[i];
            // 将创建的节点插入到页面当中
            document.body.appendChild(div);
        }
    }
    // 执行插入节点的操作
    inset(cityData);

  要解决这个问题其实也比较的简单,既然一次添加多个节点容易造成浏览器的卡顿或假死,那就将节点分为多批次添加到页面当如,如每次插入固定的个数,分批添加,直至全部完成。

    /**
     * timeChunk() 分批次加载
     * fn 数据处理函数
     * count 每次加载数量
     * interval 设置时间间隔
     */
    function timeChunk(fn, count = 10, interval = 100) {
        var time = null,
            _self = this;
        
        // ary 需要进行处理的所有数据
        return function (ary) {
            var newData = [].concat(ary),
                computData = null;

            (function func() {
                let l = newData.length;
                if (!l) {
                    newData = null; // 这里不知道是否有必要手动释放内存
                    return clearTimeout(time);
                }
                // 数据切割
                computData = newData.splice(0, Math.min(count, l));
                // 这里调用原始数据,将切割完成的数据传入
                fn.call(_self, computData);
                setTimeout(function () {
                    func();
                }, interval);
            })();
        }
    }
    var insetList = function (listData) {
        for (var i = 0, l = listData.length; i < l; i++) {
            var div = document.createElement('div');
            div.innerHTML = listData[i]['text'];
            document.body.appendChild(div);
        }
    }
    // 数据模拟
    var listData = [];
    for (var i = 0; i < 500; i++) {
        listData[i] = {
            text: i
        };
    }

    // 对原函数进行封装
    insetList = timeChunk(insetList);
    // 将数据加载到页面当中
    insetList(listData);

懒加载

  在说什么是懒加载之前先来说一种现象。在开发的过程中会因为浏览器之间的差异或者兼容性问题做一些函数的封装,用于处理不同场景或者浏览器环境问题。比如,封装 ajax方法,需要判断是否支持 XMLHttpRequest 如果不支持则替换成 ActiveXObject

    var xmlHttp = function(){
        return window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
    }

  这是比较常见的嗅探工作,还有比如事件委派中用到的 addEventListener 并非是所有的浏览器都支持的,所以需要先进行处理返回浏览器支持的函数在进行事件委派操作等。

  封装的函数确实能够为正常的工作带来便利,但可能又带来另一方面的问题,就是在每次调用 xmlHttp 方法的时候都要执行一次嗅探工作,无疑增加了浏览器的运行成本,虽然这个成本可以忽略。那么有什么方法可以避免每次调用前的嗅探工作呢?那就是在统一完成一次嗅探,之后的每次都直接调用嗅探后的即可。

    // 在页面开始位置执行
    var xmlHttp = (function(){
        return window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
    })();

  到这里就结束了吗,显然没有。这个页面是否需要调取ajax还不确定,什么时候调用,在哪里用都不知道,如果页面不需要使用ajax请求,那么这个嗅探就是无用的。

  解决这个问题的方法就是当第一次调用的时候执行嗅探,嗅探完成的通知修改原函数(这里利用了作用域链),在之后的调用都是该写后的函数了,也就避免了重复嗅探或者无用的嗅探操作。

    var btn = document.getElementById('btn');
    var addEvent = function (elem, type, handler) {
        // 当第一次调用时在这里改写
        if (window.addEventListener) {
            addEvent = function (elem, type, handler) {
                elem.addEventListener(type, handler, false);
            }
        } else if (window.attachEvent) {
            addEvent = function (elem, type, handler) {
                elem.attachEvent('on' + type, handler);
            }
        }
        // 可以从控制台中看到,调用两次,只有一次打印
        console.info(addEvent);
        // 这个时候,原来的 addEvent 函数已经被改写了
        // 改写成功后在这里调用
        // 如果这里不调用的话第一次执行只是对函数进行了该写
        // 不会委派事件
        addEvent(elem, type, handler);
    }

    // 第一次调用
    addEvent(btn, 'click', function (em) {
        console.info(em);
    });
    // 第二次调用
    addEvent(btn, 'click', function (em) {
        console.info('em');
    });