liujs

liujs 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

liujs 赞了文章 · 1月6日

探究防抖(debounce)和节流(throttle)

本文来自我的博客,欢迎大家去GitHub上star我的博客

本文从防抖和节流出发,分析它们的特性,并拓展一种特殊的节流方式requestAnimationFrame,最后对lodash中的debounce源码进行分析

防抖和节流是前端开发中经常使用的一种优化手段,它们都被用来控制一段时间内方法执行的次数,可以为我们节省大量不必要的开销

防抖(debounce)

当我们需要及时获知窗口大小变化时,我们会给window绑定一个resize函数,像下面这样:

window.addEventListener('resize', () => {
    console.log('resize')
});

我们会发现,即使是极小的缩放操作,也会打印数十次resize,也就是说,如果我们需要在onresize函数中搞一些小动作,也会重复执行几十次。但实际上,我们只关心鼠标松开,窗口停止变化的那一次resize,这时候,就可以使用debounce优化这个过程:

const handleResize = debounce(() => {
    console.log('resize');
}, 500);
window.addEventListener('resize', handleResize);

运行上面的代码(你得有现成的debounce函数),在停止缩放操作500ms后,默认用户无继续操作了,才会打印resize

这就是防抖的功效,它把一组连续的调用变为了一个,最大程度地优化了效率

再举一个防抖的常见场景:

搜索栏常常会根据我们的输入,向后端请求,获取搜索候选项,显示在搜索栏下方。如果我们不使用防抖,在输入“debounce”时前端会依次向后端请求"d"、"de"、"deb"..."debounce"的搜索候选项,在用户输入很快的情况下,这些请求是无意义的,可以使用防抖优化

观察上面这两个例子,我们发现,防抖非常适于只关心结果,不关心过程如何的情况,它能很好地将大量连续事件转为单个我们需要的事件

为了更好理解,下面提供了最简单的debounce实现:返回一个function,第一次执行这个function会启动一个定时器,下一次执行会清除上一次的定时器并重起一个定时器,直到这个function不再被调用,定时器成功跑完,执行回调函数

const debounce = function(func, wait) {
    let timer;
    return function() {
        !!timer && clearTimeout(timer);
        timer = setTimeout(func, wait);
    };
};

那如果我们不仅关心结果,同时也关心过程呢?

节流(throttle)

节流让指定函数在规定的时间里执行次数不会超过一次,也就是说,在连续高频执行中,动作会被定期执行。节流的主要目的是将原本操作的频率降低

实例:

我们模拟一个可无限滚动的feed流

html:

<div id="wrapper">
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
</div>

css:

#wrapper {
    height: 500px;
    overflow: auto;
}
.feed {
    height: 200px;
    background: #ededed;
    margin: 20px;
}

js:

const wrapper = document.getElementById("wrapper");
const loadContent = () => {
    const {
        scrollHeight,
        clientHeight,
        scrollTop
    } = wrapper;
    const heightFromBottom = scrollHeight - scrollTop - clientHeight;
    if (heightFromBottom < 200) {
        const wrapperCopy = wrapper.cloneNode(true);
        const children = [].slice.call(wrapperCopy.children);
        children.forEach(item => {
            wrapper.appendChild(item);
        })
    }
}
const handleScroll = throttle(loadContent, 200);
wrapper.addEventListener("scroll", handleScroll);

可以看到,在这个例子中,我们需要不停地获取滚动条距离底部的高度,以判断是否需要增加新的内容。我们知道,srcoll同样也是种会高频触发的事件,我们需要减少它有效触发的次数。如果使用的是防抖,那么得等我们停止滚动之后一段时间才会加载新的内容,没有那种无限滚动的流畅感。这时候,我们就可以使用节流,将事件有效触发的频率降低的同时给用户流畅的浏览体验。在这个例子中,我们指定throttle的wait值为200ms,也就是说,如果你一直在滚动页面,loadCotent函数也只会每200ms执行一次

同样,这里有throttle最简单的实现,当然,这种实现很粗糙,有不少缺陷(比如没有考虑最后一次执行),只供初步理解使用:

const throttle = function (func, wait) {
    let lastTime;
    return function () {
        const curTime = Date.now();
        if (!lastTime || curTime - lastTime >= wait) {
            lastTime = curTime;
            return func();
        }
    }
}

requestAnimationFrame(rAF)

rAF在一定程度上和throttle(func,16)的作用相似,但它是浏览器自带的api,所以,它比throttle函数执行得更加平滑。调用window.requestAnimationFrame(),浏览器会在下次刷新的时候执行指定回调函数。通常,屏幕的刷新频率是60hz,所以,这个函数也就是大约16.7ms执行一次。如果你想让你的动画更加平滑,用rAF就再好不过了,因为它是跟着屏幕的刷新频率来的

rAF的写法与debounce和throttle不同,如果你想用它绘制动画,需要不停地在回调函数里调用自身,具体写法可以参考mdn

rAF支持ie10及以上浏览器,不过因为是浏览器自带的api,我们也就无法在node中使用它了

总结

debounce将一组事件的执行转为最后一个事件的执行,如果你只关注结果,debounce再适合不过

如果你同时关注过程,可以使用throttle,它可以用来降低高频事件的执行频率

如果你的代码是在浏览器上运行,不考虑兼容ie10,并且要求页面上的变化尽可能的平滑,可以使用rAF

参考:https://css-tricks.com/debouncing-throttling-explained-examples/

附:lodash源码解析

lodash的debounce功能十分强大,集debounce、throttle和rAF于一身,所以我特意研读一下,下面是我的解析(我删去了一些不重要的代码,比如debounced的cancel方法):

function debounce(func, wait, options) {
    /**
     * lastCallTime是上一次执行debounced函数的时间
     * lastInvokeTime是上一次调用func的时间
     */
    let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;

    let lastInvokeTime = 0;
    let leading = false;
    let maxing = false;
    let trailing = true;

    /**
     * 如果没设置wait且raf可用 则默认使用raf
     */
    const useRAF =
        !wait && wait !== 0 && typeof root.requestAnimationFrame === "function";

    if (typeof func !== "function") {
        throw new TypeError("Expected a function");
    }
    wait = +wait || 0;
    if (isObject(options)) {
        leading = !!options.leading;
        maxing = "maxWait" in options;
        maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
        trailing = "trailing" in options ? !!options.trailing : trailing;
    }

    /**
     * 执行func
     */
    function invokeFunc(time) {
        const args = lastArgs;
        const thisArg = lastThis;

        lastArgs = lastThis = undefined;
        /**
         * 更新lastInvokeTime
         */
        lastInvokeTime = time;
        result = func.apply(thisArg, args);
        return result;
    }

    /**
     * 调用定时器
     */
    function startTimer(pendingFunc, wait) {
        if (useRAF) {
            root.cancelAnimationFrame(timerId);
            return root.requestAnimationFrame(pendingFunc);
        }
        return setTimeout(pendingFunc, wait);
    }

    /**
     * 在每轮debounce开始调用
     */
    function leadingEdge(time) {
        lastInvokeTime = time;
        timerId = startTimer(timerExpired, wait);
        return leading ? invokeFunc(time) : result;
    }

    /**
     * 计算剩余时间
     * 1是 wait 减去 距离上次调用debounced时间(lastCallTime)
     * 2是 maxWait 减去 距离上次调用func时间(lastInvokeTime)
     * 1和2取最小值
     */
    function remainingWait(time) {
        const timeSinceLastCall = time - lastCallTime;
        const timeSinceLastInvoke = time - lastInvokeTime;
        const timeWaiting = wait - timeSinceLastCall;

        return maxing
            ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
            : timeWaiting;
    }

    /**
     * 判断是否需要执行
     */
    function shouldInvoke(time) {
        const timeSinceLastCall = time - lastCallTime;
        const timeSinceLastInvoke = time - lastInvokeTime;
        /**
         * 4种情况返回true,否则返回false
         * 1.第一次调用
         * 2.距离上次调用debounced时间(lastCallTime)>=wait
         * 3.系统时间倒退
         * 4.设置了maxWait,距离上次调用func时间(lastInvokeTime)>=maxWait
         */
        return (
            lastCallTime === undefined ||
            timeSinceLastCall >= wait ||
            timeSinceLastCall < 0 ||
            (maxing && timeSinceLastInvoke >= maxWait)
        );
    }

    /**
     * 通过shouldInvoke函数判断是否执行
     * 执行:调用trailingEdge函数
     * 不执行:调用startTimer函数重新开始timer,wait值通过remainingWait函数计算
     */
    function timerExpired() {
        const time = Date.now();
        if (shouldInvoke(time)) {
            return trailingEdge(time);
        }
        // Restart the timer.
        timerId = startTimer(timerExpired, remainingWait(time));
    }

    /**
     * 在每轮debounce结束调用
     */
    function trailingEdge(time) {
        timerId = undefined;

        /**
         * trailing为true且lastArgs不为undefined时调用
         */
        if (trailing && lastArgs) {
            return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
    }

    function debounced(...args) {
        const time = Date.now();
        const isInvoking = shouldInvoke(time);

        lastArgs = args;
        lastThis = this;
        /**
         * 更新lastCallTime
         */
        lastCallTime = time;

        if (isInvoking) {
            /**
             * 第一次调用
             */
            if (timerId === undefined) {
                return leadingEdge(lastCallTime);
            }
            /**
             * 【注1】
             */
            if (maxing) {
                timerId = startTimer(timerExpired, wait);
                return invokeFunc(lastCallTime);
            }
        }
        /**
         * 【注2】
         */
        if (timerId === undefined) {
            timerId = startTimer(timerExpired, wait);
        }
        return result;
    }
    return debounced;
}

推荐是从返回的方法debounced开始,顺着执行顺序阅读,理解起来更轻松

【注1】一开始我没看明白if(maxing)里面这段代码的作用,按理说,是不会执行这段代码的,后来我去lodash的仓库里看了test文件,发现对这段代码,专门有一个case对其测试。我剥除了一些代码,并修改了测试用例以便展示,如下:

var limit = 320,
    withCount = 0

var withMaxWait = debounce(function () {
    console.log('invoke');
    withCount++;
}, 64, {
    'maxWait': 128
});

var start = +new Date;
while ((new Date - start) < limit) {
    withMaxWait();
}

执行代码,打印了3次invoke;我又将if(maxing){}这段代码注释,再执行代码,结果只打印了1次。结合源码的英文注释Handle invocations in a tight loop,我们不难理解,原本理想的执行顺序是withMaxWait->timer->withMaxWait->timer这种交替进行,但由于setTimeout需等待主线程的代码执行完毕,所以这种短时间快速调用就会导致withMaxWait->withMaxWait->timer->timer,从第二个timer开始,由于lastArgs被置为undefined,也就不会再调用invokeFunc函数,所以只会打印一次invoke。

同时,由于每次执行invokeFunc时都会将lastArgs置为undefined,在执行trailingEdge时会对lastArgs进行判断,确保不会出现执行了if(maxing){}中的invokeFunc函数又执行了timer的invokeFunc函数

这段代码保证了设置maxWait参数后的正确性和时效性

【注2】执行过一次trailingEdge后,再执行debounced函数,可能会遇到shouldInvoke返回false的情况,需单独处理

【注3】对于lodash的debounce来说,throttle是一种leading为true且maxWait等于wait的特殊debounce

查看原文

赞 1 收藏 0 评论 1

liujs 关注了专栏 · 2019-07-15

曾会玩

玩算法

关注 11

liujs 赞了回答 · 2019-05-28

解决iview的input标签报错 x-invalid-end-tag

.eslintrc.js 的 rules 加上

"vue/no-parsing-error": [2, { "x-invalid-end-tag": false }]

其他解释可见 iview--issue

关注 6 回答 3

liujs 关注了专栏 · 2019-05-07

网易考拉前端技术团队

团队知识的积累与分享,起于技术但不局限于技术。

关注 15

liujs 赞了文章 · 2019-02-23

手写数组操作常见方法的polyfill

前言:在JS操作当中,前端开发人员对数组的操作特别频繁,随着每一次ECMAScript的发版,对于数组所提供的API也会增多,以增强数组的操作能力。下面整理了一些在工作当中常用的数据操作方法的polyfill,方便大家更好的掌握其原理。

数组map和forEach方法

forEach方法的polyfill

Array.prototype.forEachFn = function(callback){
  for(var i = 0;i< this.length;i++){
    callback.call(this,this[i],i,this);
  }
};

var list = [1,2,4,8,10];
list.forEachFn(function(item,index,arr){
  return list[index] = item*2;
});  //无返回值

[2, 4, 8, 16, 20] //list

map方法的polyfill

Array.prototype.mapFn = function(callback){
var newArr = [];
 for(var i = 0;i< this.length;i++){
  newArr.push(callback.call(this,this[i],i,this));
 }
 return newArr;
}


 var list = [1,3,5,7,9];
 list.mapFn(function(item){
   return item*2;
 });     //执行完后返回一个新数组 [2, 6, 10, 14, 18]
 

再看看两者的执行速度:
图片描述

可以看出forEach()的执行速度比map()慢了70%

相同点:
能用forEach()做到的,map()同样可以。反过来也是如此。

差异:

  1. forEach没有返回值,但可在callback里改变原数组,map返回一个新数组,不改变原数组,可链式调用数组的其他方法。
  2. map执行速度比forEach更快。

数组every和some方法

every方法的polyfill

Array.prototype.everyFn = function(callback){
  for(var i = 0;i<this.length;i++){
    if(!callback.call(this,this[i],i,this)){
     return false;
    }
    
  }
 return true;
}
var list = [1,3,5,7,9];
list.everyFn((item)=>{
 return item>3
}); //返回false

some方法的polyfill

Array.prototype.someFn = function(callback){
 for(var i = 0;i<this.length;i++){
   if(callback.call(this,this[i],i,this)){
     return true;
    }
 }
 return false;
}

var list = [1,2,4,8,10];
list.someFn(item =>item>4) //返回true

var list = [1,2,4,8,10];
list.someFn(item =>item>12) //返回false

方法区别
every() 每一项都返回true才返回true
some() 只要有一项返回true就返回true

数组reduce和reduceRight方法

reduce方法的polyfill

Array.prototype.reduceFn = function(callback,initValue){
  var preValue = initValue || this[0];
  for(var i = initValue ? 0 : 1; i<this.length; i++){
    preValue = callback(preValue,this[i],i,this);
  }
  return preValue;
 }

求和:

var list = [1,3,5,7,9];
list.reduceFn(function(prev,current,currentIndex,arr){
  return prev+current;
});  //返回25

求和+10:

var list = [1,3,5,7,9];
list.reduceFn(function(prev,current,currentIndex,arr){
  return prev+current;
},10); //返回35

reduceRight方法的polyfill

Array.prototype.reduceRightFn = function(callback,initValue){
  var lastIndex = this.length - 1;
  var preValue = initValue || this[lastIndex];
  console.log(preValue);
  for(var i = initValue ? 0 : 1; i<this.length; i++){
    preValue = callback(preValue,this[lastIndex-i],i,this);
  }
  return preValue;
 }

求和:

var list = [1,3,5,7,9];
list.reduceRightFn(function(prev,current,currentIndex,arr){
  return prev+current;
});  //返回25

求和+10:

var list = [1,3,5,7,9];
list.reduceRightFn(function(prev,current,currentIndex,arr){
  return prev+current;
},10); //返回35

数组reduce方法其作用是对数组进行归并操作,传递两个数组,第一个是callback,第二个参数可选(初始值)。其中回调函数 callback 接收4个参数:

1.previousValue - 存放的是上一次callback返回的结果,其初始值默认为数组的第一个元素。
2.currentValue - 是当前元素 。默认从数组的第二个元素开始。
3.currentIndex - 是当前元素位置 。
4.array - 是当前数组。

今天就整理这么多了,如果有时间,我会继续丰富本页面,以提供更全的资料供大家参考,如果喜欢我的文章,请Star!!!/::)

查看原文

赞 2 收藏 0 评论 1

liujs 关注了用户 · 2019-02-12

冴羽 @yayu

JavaScript深入系列 15 篇已完结。

JavaScript专题系列 20 篇已完结。

underscore 系列 8 篇已完结。

ES6 系列 20 篇已完结。

现在写起了各种单篇……

React 系列还在懒惰中……

公众号:冴羽的JavaScript博客

关注 2030

liujs 关注了用户 · 2019-02-02

边城 @jamesfancy

从事软件开发 20 年,在软件分析、设计、架构、开发及软件开发技术研究和培训等方面有着非常丰富的经验,近年主要在研究 Web 前端技术、基于 .NET 的后端开发技术和相关软件架构。欢迎搜索并关注公众号“边城客栈” ⇐ 点这里

关注 10910

liujs 回答了问题 · 2019-01-12

解决html table 里面的 tr 如何固定高度?

关注 6 回答 5

liujs 关注了用户 · 2018-12-10

Aaaaaaaty @aaaaaaaty

天猫营销平台持续招人中,服务端、客户端、前端、算法;base 杭州;有需要请发送简历到tianyu.aty@alibaba-inc.com;注明来自[sf]

关注 2010

liujs 关注了专栏 · 2018-11-09

劝君惜取少年时

不搞文艺的程序员不是个好厨子,一名转行码届的程序猿的随笔

关注 83

认证与成就

  • 获得 16 次点赞
  • 获得 26 枚徽章 获得 2 枚金徽章, 获得 7 枚银徽章, 获得 17 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-11-20
个人主页被 555 人浏览