Question of the day: How to write anti-shake function and throttling function

foreword

Although anti-shake and throttling can already be implemented by introducing Loadsh globally, handwritten anti-shake and throttling are often encountered during interviews.

In fact, 防抖and 节流not only will let everyone write by hand in the interview, but also can play a role in performance optimization in actual projects, so it is still necessary to master. On the one hand, they have many application scenarios, and on the other hand, the code behind them also has certain difficulties: high-order functions, timers, logic, context, etc., which are used to evaluate the basic skills and practical ability of the interviewee. choose.

chestnut

 <style>
    #content {
        height: 400px;
        width: 400px;
        margin: 0 auto;
        line-height: 400px;
        text-align: center;
        color: #fff;
        background-color: pink;
        font-size: 80px;
    }
</style>

<div id="content">
</div>
<script>

    let num = 1;
    const content = document.getElementById('content');

    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = count

</script>
复制代码

In the case of no other processing, the function is executed frequently causing the data on the page to change very quickly. So, let's take a look at how anti-shake and throttling solve this problem.

The effect will not be shown here. If you are interested, you can copy and see the running result.

anti-shake

The so-called anti-shake means that the function is executed n seconds after the event is triggered. If the event is triggered again within n seconds, the function execution time will be recalculated.

The anti-shake function is divided into non-immediate execution version and immediate execution version

non-immediate execution

   <style>
        #content {
            height: 400px;
            width: 400px;
            margin: 0 auto;
            line-height: 400px;
            text-align: center;
            color: #fff;
            background-color: pink;
            font-size: 80px;
        }
    </style>
    
<div id="content">
</div>
<script>
    /**
        * @description: 防抖函数  非立即执行
        * @param {type} fn  函数
        * @param {type} wait  延迟时间 单位毫秒
        * @return: 返回 延时函数
        */
    // 非立即执行
    let num = 1;
    const content = document.getElementById('content');
    function debounce(fn, wait) {
        let timerID
        return function () {
            
            // 如果有定时器id清除定时器
            if (timerID) clearTimeout(timerID)
            timerID = setTimeout(() => {
                // 修改this指向
                fn()
            }, wait)
        }
    }
    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = debounce(count, 1000)

</script>
复制代码
  1. The anti-shake function is completed, but there will be a problem. If we are count()printing , we thiswill find that what we are doing thisis pointing Windowto

  2. Not what we want, we need to use applyto change thisthe direction, and we need to take into account the parameters of the execution function, because different functions will definitely have different parameters passed in, for parameters we can use argumentsprocessing

full version

<style>
        #content {
            height: 400px;
            width: 400px;
            margin: 0 auto;
            line-height: 400px;
            text-align: center;
            color: #fff;
            background-color: pink;
            font-size: 80px;
        }
    </style>
    
<div id="content">
</div>
<script>
    /**
        * @description: 防抖函数  非立即执行
        * @param {type} fn  函数
        * @param {type} wait  延迟时间 单位毫秒
        * @return: 返回 延时函数
        */
    // 非立即执行
    let num = 1;
    const content = document.getElementById('content');
    function debounce(fn, wait) {
        let timerID
        return function () {
            // this指向依然指向原来的函数
            const context = this
            // // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
            const args = [...arguments]
            // 如果有定时器id清除定时器
            if (timerID) clearTimeout(timerID)
            timerID = setTimeout(() => {
                // 修改this指向
                fn.apply(context, args)
            }, wait)
        }
    }
    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = debounce(count, 1000)

</script>
复制代码

Non-immediate execution means that the function will not be executed immediately after the event is triggered, but will be executed after n seconds. If the event is triggered again within n seconds, the function execution time will be recalculated.

execute immediately

The immediate execution version means that the function will be executed immediately after the event is triggered, and then the effect of the function can continue to be executed if the event is not triggered within n seconds.

<style>
    #content {
        height: 400px;
        width: 400px;
        margin: 0 auto;
        line-height: 400px;
        text-align: center;
        color: #fff;
        background-color: pink;
        font-size: 80px;
    }
</style>

 <div id="content">
</div>
<script>
    /**
    * @description: 防抖函数  立即执行
    * @param {type} fn  函数
    * @param {type} wait  延迟时间 单位毫秒
    * @return: 返回 延时函数
    */
    // 立即执行
    let num = 1;
    const content = document.getElementById('content');
    function debounce(fn, wait) {
        let timerID
        return function () {
            // this指向依然指向原来的函数
            const context = this
            // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
            const args = [...arguments]
            // 如果有定时器id清除定时器
            if (timerID) clearTimeout(timerID)
            const callNow = !timerID
            timerID = setTimeout(() => {
                // 相当于清空定时器
                timerID = null
            }, wait)
            // 没有定时器id  修改this指向
            if (callNow) fn.apply(context, args)
        }
    }
    function count() {
        content.innerHTML = num++;
    };
    content.onmousemove = debounce(count, 1000)

</script>
复制代码

merge

In the development process, we need to decide which version of the anti-shake function we need to use according to different scenarios. Generally speaking, the above anti-shake functions can meet the needs of most scenarios. But we can also combine the non-immediate version and the immediate version of the anti-shake function

    <style>
    #content {
        height: 400px;
        width: 400px;
        margin: 0 auto;
        line-height: 400px;
        text-align: center;
        color: #fff;
        background-color: pink;
        font-size: 80px;
    }
</style>

 <div id="content">
</div>
<script>
    /**
        * @description: 防抖函数  非立即执行
        * @param {type} fn  函数
        * @param {type} wait  延迟时间 单位毫秒
        * @param {type} promptly  true : 立即  false 非立即
        * @return: 返回 延时函数
        */
    // 非立即执行
    let num = 1;
    const content = document.getElementById('content');
    function debounce(fn, wait, promptly) {
        let timerID
        return function () {
            // this指向依然指向原来的函数
            const context = this
            // // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
            const args = [...arguments]
            // 如果有定时器id清除定时器
            if (timerID) clearTimeout(timerID)
            if (promptly) {
                const callNow = !timerID
                timerID = setTimeout(() => {
                    // 相当于清空定时器
                    timerID = null
                }, wait)
                // 没有定时器id  修改this指向
                if (callNow) fn.apply(context, args)
            } else {
                timerID = setTimeout(() => {
                    // 修改this指向
                    fn.apply(context, args)
                }, wait)
            }
        }
    }
    function count() {
        content.innerHTML = num++;
        console.log(this);
    };

    content.onmousemove = debounce(count, 1000, true)

</script>
复制代码

throttling

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。  节流会稀释函数的执行频率。

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳

 <style>
        #content {
            height: 400px;
            width: 400px;
            margin: 0 auto;
            line-height: 400px;
            text-align: center;
            color: #fff;
            background-color: pink;
            font-size: 80px;
        }
    </style>
    
<div id="content">
</div>
<script>
    /**
        * @description: 节流函数  时间戳
        * @param {type} fn  函数
        * @param {type} wait  延迟时间 单位毫秒
        * @return: 返回 延时函数
        */
    // 时间戳
    let num = 1;
    const content = document.getElementById('content');
    function throttle(fn, wait, promptly) {
    let previous = 0
    return function () {
        let now = Date.now()
        // this指向依然指向原来的函数
        let context = this
        // // 不同的函数会有不同的参数传入,对于参数我使用arguments处理。
        let args = [...arguments]
        if (now - previous > wait) {
            // 修改this指向
            fn.apply(context, args)
            // 修改时间为当前时间
            previous = now
        }

    }
}
    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = throttle(count, 1000)

</script>
复制代码

在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

定时器版

<style>
        #content {
            height: 400px;
            width: 400px;
            margin: 0 auto;
            line-height: 400px;
            text-align: center;
            color: #fff;
            background-color: pink;
            font-size: 80px;
        }
    </style>
    
<div id="content">
</div>
<script>
    /**
        * @description: 节流函数  定时器版
        * @param {type} fn  函数
        * @param {type} wait  延迟时间 单位毫秒
        * @return: 返回 延时函数
        */
    // 定时器版
    let num = 1;
    const content = document.getElementById('content');
    function throttle(fn, wait) {
    let timerID;
    return function () {
        // this指向依然指向原来的函数
        let context = this;
        // 不同的函数会有不同的参数传入,对于参数我使用arguments处理
        let args = [...arguments];
        if (!timerID) {
            timerID = setTimeout(() => {
                timerID = null;
                fn.apply(context, args)
            }, wait)
        }

    }
}
    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = throttle(count, 1000)

</script>
复制代码

在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次

其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候

和防抖函数一样,也可以进行合并

合并

   <style>
            #content {
                height: 400px;
                width: 400px;
                margin: 0 auto;
                line-height: 400px;
                text-align: center;
                color: #fff;
                background-color: pink;
                font-size: 80px;
            }
        </style>
    
    <div id="content">
    </div>
    <script>
    //**
    * @description: 节流函数  合并版
    * @param {type} fn  函数
    * @param {type} wait  延迟时间 单位毫秒
    * @param {type} type 1 表时间戳版,2 表定时器版
    * @return: 返回 延时函数
    */
   
    let num = 1;
    const content = document.getElementById('content');
    function throttle(fn, wait, type) {
    if (type === 1) {
        let previous = 0;
    } else if (type === 2) {
        let timerID;
    }
    return function () {
            // this指向依然指向原来的函数
            let context = this;
            // 不同的函数会有不同的参数传入,对于参数我使用arguments处理
            if (type === 1) {
                let now = Date.now();
                if (now - previous > wait) {
                    func.apply(context, args);
                    // 修改时间为当前时间
                    previous = now;
                }
            } else if (type === 2) {
                if (!timerID) {
                    timerID = setTimeout(() => {
                        timerID = null;
                        func.apply(context, args)
                    }, wait)
                }
            }
        }
    }
    function count() {
        content.innerHTML = num++;
    };

    content.onmousemove = throttle(count, 1000)

</script>
复制代码

总结

  1. 对于防抖节流一个最主观的判断方法就是:防抖则会只执行一次,而节流则会每隔一段时间执行一次
  • 其实本质上都是为了节省程序的性能(防止高频函数调用)

  • 借助了闭包的特性来缓存变量(状态)

  • 都可以使用setTimeout实现

Guess you like

Origin juejin.im/post/7084235599074295839