js anti shake throttle

closure

Since closures are used in the implementation of throttling and anti shake functions, we will briefly introduce what closures are before understanding throttling and anti shake functions.

function Add() {
    var x = 1;
    return function () {
        x++;
        console.log(x);
    }
}
var result = Add();  //Execute A function for the first time    
result();  //2
result();  //3

The above code: result points to the function returned by function Add. After running Add(), the execution environment of Add will be released. However, since there is a reference to variable x in the function returned in function Add, x will not be released during release. Each time result() is called, the reference to x will be maintained, so x will continue to increase.

Anti shake

definition

  • Only when a function is not triggered again within a certain time can it be called; Let's use a picture to understand its process;
  • When an event is triggered, the corresponding function will not be triggered immediately, but will wait for a certain time;
  • When events are triggered intensively, the triggering of functions will be frequently delayed;
  • Only after waiting for a period of time and no event is triggered can the response function be truly executed;

Anti shake function

There are many application scenarios for anti shake:

  • Frequently input content in the input box, search or submit information;
  • Click the button frequently to trigger an event;
  • Monitor browser scrolling events and complete some specific operations;
  • resize event when the user zooms the browser;

In short, for dense event triggering, we only want to trigger events that occur later, so we can use the anti shake function;

Within the specified time, only the last one will take effect, and the previous one will not take effect.

Initial implementation

<body>
    <input type="text" class="search">

    <script>
        var timer
        function debounce() {
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(function () {
                ajax()
            }, 1000);
        }

        function ajax() {
            console.log('ajaxajaxajax');
        }
        
        var search = document.querySelector('.search')
        search.addEventListener('input', debounce)
        
    </script>
</body>

The above code is the simplest anti shake function, but the following problems will occur: 1. When multiple anti shake functions are required on a page, you need to write a lot of duplicate code. Cannot reuse 2. Global variable pollution scope

Therefore, there are the following upgrades

Implementation 1: optimize global variable pollution

<body>
    <input type="text" class="search">

    <script>

        function debounce() {
            var timer
            return function () {
                if (timer) {
                    clearTimeout(timer)
                }
                timer = setTimeout(function () {
                    ajax()
                }, 1000);
            }

        }

        function ajax() {
            console.log('ajaxajaxajax');
        }

        var search = document.querySelector('.search')
        search.addEventListener('input', debounce())

    </script>
</body>

Implementation 2: optimization can define the function to be executed and the anti shake time

<body>
    <input type="text" class="search">

    <script>
        // Optimization defines the function to be executed and the anti shake time
        function debounce(fn, delay) {
            var timer
            return function () {
                if (timer) {
                    clearTimeout(timer)
                }
                timer = setTimeout(function () {
                    fn()
                }, delay);
            }

        }

        function ajax(arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);          // window
            console.log(arg1, arg2);    // undefined undefined
        }

        var search = document.querySelector('.search')
        search.addEventListener('input', debounce(ajax, 500))

    </script>
</body>

Implementation 3: optimize this and arguments

<body>
    <input type="text" class="search">

    <script>

        // Optimize this and arguments
        function debounce(fn, delay) {
            var timer
            return function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments
                timer = setTimeout(function () {
                    fn.apply(_this, _arguments)
                }, delay);
            }

        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);          // <input type="text" class="search">
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 500)


        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200])
        })

        // This is OK, but pay attention to the change of parameter position
        // Arguments(3) [100, 200, InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
        // search.addEventListener('input', debounceWrap.bind(search, 100, 200))

    </script>
</body>

Implementation 4: optimize the immediate execution of the head

<body>
    <input type="text" class="search">

    <script>
        // Optimize the head and execute immediately
        function debounce(fn, delay, leading) {
            var timer
            var leading = leading || false
            return function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments
                if (leading) {
                    // A variable is used to record whether to execute immediately
                    var isFirst = false

                    // If the timer is fasle, it will be executed immediately (it is undefined when anti shake is executed for the first time, so it will be executed for the first time)
                    if (!timer) {
                        fn.apply(_this, _arguments)
                        isFirst = true
                    }
                    // The rest are delayed
                    timer = setTimeout(function () {

                        // After the first anti shake is performed immediately, the timer is not fasle,
                        // Set timer to null, so that the next time anti shake is triggered, immediate execution will take effect
                        timer = null

                        // Judge whether anti shake continues to be triggered after immediate execution, and execute only if it does,
                        // If the immediate execution ends after only one execution, it will not be executed
                        if (!isFirst) {
                            fn.apply(_this, _arguments)
                        }
                    }, delay);

                } else {
                    timer = setTimeout(function () {
                        fn.apply(_this, _arguments)
                    }, delay);
                }
            }

        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);  // <input type="text" class="search">
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 1000, true)



        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200])
        })

        // This is OK, but pay attention to the change of parameter position
        // Arguments(3) [100, 200, InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
        // search.addEventListener('input', debounce(ajax, 1000, true).bind(search, 100, 200))

    </script>
</body>

Realization 5: optimize midway cancellation

<body>
    <input type="text" class="search">
    <button class="cancel">cancel</button>

    <script>
        // Optimize the head and execute immediately
        function debounce(fn, delay, leading) {
            var timer
            var leading = leading || false
            var debounceFn = function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments

                if (leading) {
                    // A variable is used to record whether to execute immediately
                    var isFirst = false

                    // If the timer is fasle, it will be executed immediately (it is undefined when anti shake is executed for the first time, so it will be executed for the first time)
                    if (!timer) {
                        fn.apply(_this, arguments)
                        isFirst = true
                    }
                    // The rest are delayed
                    timer = setTimeout(() => {

                        // After the first anti shake is performed immediately, the timer is not fasle,
                        // Set timer to null, so that the next time anti shake is triggered, immediate execution will take effect
                        timer = null

                        // Judge whether anti shake continues to be triggered after immediate execution, and execute only if it does,
                        // If the immediate execution ends after only one execution, it will not be executed
                        if (!isFirst) {
                            fn.apply(_this, arguments)
                        }
                    }, delay);

                } else {
                    timer = setTimeout(() => {
                        fn.apply(_this, arguments)
                    }, delay);
                }
            }

            debounceFn.cancel = function () {
                clearTimeout(timer)
                // After clearing the timer, the timer is still a value, which will invalidate the next immediate execution
                // Therefore, after clearing the timer, set the timer to null
                // The above steps should not be disordered. You should clear them first and then assign null again
                timer = null
            }
            return debounceFn
        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);  // <input type="text" class="search">
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 1000, true)


        // Note: since the function has its own scope, if anti shake and midway cancellation are defined separately,
        // Then they do not point to the same scope, which will lead to the invalidation of Midway cancellation function
        // If you want to use the midway cancellation function, you must define a variable externally to save the anti shake function
        // When performing anti shake and midway cancellation, you have to operate through external global variables!
        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200])
        })

        // This is OK, but pay attention to the change of parameter position
        // Arguments(3) [100, 200, InputEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
        // search.addEventListener('input', debounceWrap.bind(search, 100, 200))

        // This anti shake is OK, but it can't be cancelled halfway. Wrong writing!
        // search.addEventListener('input', debounce(ajax, 1000, true).bind(search, 100, 200))

        var cancel = document.querySelector('.cancel')

        cancel.addEventListener('click', function () {
            debounceWrap.cancel()
        })

        // This can also be cancelled
        // cancel.addEventListener('click', debounceWrap.cancel)

        // In this way, the midway cancellation will be executed, but the cancellation is not the same anti shake function, wrong writing!
        // cancel.addEventListener('click', debounce(ajax, 1000, true).cancel)


    </script>
</body>

Implementation 6: optimize the return value (callback version)

<body>
    <input type="text" class="search">
    <button class="cancel">cancel</button>

    <script>
        // Optimize return value
        function debounce(fn, delay, option) {
            var timer
            var option = option || {}
            var leading = option.leading || false
            var callback = option.callback || null
            var result
            var debounceFn = function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments
                if (leading) {
                    // A variable is used to record whether to execute immediately
                    var isFirst = false

                    // If the timer is fasle, it will be executed immediately (it is undefined when anti shake is executed for the first time, so it will be executed for the first time)
                    if (!timer) {
                        result = fn.apply(_this, arguments)
                        if (callback) {
                            callback(result)
                        }
                        isFirst = true
                    }
                    // The rest are delayed
                    timer = setTimeout(() => {

                        // After the first anti shake is performed immediately, the timer is not fasle,
                        // Set timer to null, so that the next time anti shake is triggered, immediate execution will take effect
                        timer = null

                        // Judge whether anti shake continues to be triggered after immediate execution, and execute only if it does,
                        // If the immediate execution ends after only one execution, it will not be executed
                        if (!isFirst) {
                            result = fn.apply(_this, arguments)
                            if (callback) {
                                callback(result)
                            }
                        }
                    }, delay);

                } else {
                    timer = setTimeout(() => {
                        result = fn.apply(_this, arguments)
                        if (callback) {
                            callback(result)
                        }
                    }, delay);
                }

            }

            debounceFn.cancel = function () {
                clearTimeout(timer)
                // After clearing the timer, the timer is still a value, which will invalidate the next immediate execution
                // Therefore, after clearing the timer, set the timer to null
                // The above steps should not be disordered. You should clear them first and then assign null again
                timer = null
            }
            return debounceFn
        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);  // <input type="text" class="search">
            return 'I am ajax Return value of'
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 1000, {
            leading: true,
            callback: function (res) {
                console.log(res);   // I am the return value of ajax
            }
        })


        // Note: since the function has its own scope, if anti shake and midway cancellation are defined separately,
        // Then they do not point to the same scope, which will lead to the invalidation of Midway cancellation function
        // If you want to use the midway cancellation function, you must define a variable externally to save the anti shake function
        // When performing anti shake and midway cancellation, you have to operate through external global variables!
        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200])
        })

        var cancel = document.querySelector('.cancel')

        cancel.addEventListener('click', function () {
            debounceWrap.cancel()
        })
    </script>
</body>

Implementation 7: optimize the return value (Promise version)

<body>
    <input type="text" class="search">
    <button class="cancel">cancel</button>

    <script>
        // Optimize return value
        function debounce(fn, delay, leading) {
            var timer
            var leading = leading || false
            var debounceFn = function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments
                return new Promise((resolve, reject) => {
                    if (leading) {
                        // A variable is used to record whether to execute immediately
                        var isFirst = false

                        // If the timer is fasle, it will be executed immediately (it is undefined when anti shake is executed for the first time, so it will be executed for the first time)
                        if (!timer) {
                            resolve(fn.apply(_this, _arguments))
                            isFirst = true
                        }
                        // The rest are delayed
                        timer = setTimeout(() => {

                            // After the first anti shake is performed immediately, the timer is not fasle,
                            // Set timer to null, so that the next time anti shake is triggered, immediate execution will take effect
                            timer = null

                            // Judge whether anti shake continues to be triggered after immediate execution, and execute only if it does,
                            // If the immediate execution ends after only one execution, it will not be executed
                            if (!isFirst) {
                                resolve(fn.apply(_this, _arguments))
                            }
                        }, delay);

                    } else {
                        timer = setTimeout(() => {
                            resolve(fn.apply(_this, _arguments))
                        }, delay);
                    }
                })

            }

            debounceFn.cancel = function () {
                clearTimeout(timer)
                // After clearing the timer, the timer is still a value, which will invalidate the next immediate execution
                // After clearing timer, set timer to null
                // The above steps should not be disordered. You should clear them first and then assign null again
                timer = null
            }
            return debounceFn
        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);  // <input type="text" class="search">
            return 100
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 1000, true)


        // Note: since the function has its own scope, if anti shake and midway cancellation are defined separately,
        // Then they do not point to the same scope, which will lead to the invalidation of Midway cancellation function
        // If you want to use the midway cancellation function, you must define a variable externally to save the anti shake function
        // When performing anti shake and midway cancellation, you have to operate through external global variables!
        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200]).then(res => {
                console.log(res);    //100
            })
        })

        var cancel = document.querySelector('.cancel')

        cancel.addEventListener('click', function () {
            debounceWrap.cancel()
        })
    </script>
</body>

Anti shake uncommented version

<body>
    <input type="text" class="search">
    <button class="cancel">cancel</button>

    <script>
        function debounce(fn, delay, leading) {
            var timer
            var leading = leading || false
            var debounceFn = function () {
                if (timer) {
                    clearTimeout(timer)
                }
                var _this = this
                var _arguments = arguments
                return new Promise((resolve, reject) => {
                    if (leading) {
                        var isFirst = false
                        if (!timer) {
                            resolve(fn.apply(_this, _arguments))
                            isFirst = true
                        }
                        timer = setTimeout(() => {
                            timer = null
                            if (!isFirst) {
                                resolve(fn.apply(_this, _arguments))
                            }
                        }, delay);
                    } else {
                        timer = setTimeout(() => {
                            resolve(fn.apply(_this, _arguments))
                        }, delay);
                    }
                })

            }

            debounceFn.cancel = function () {
                clearTimeout(timer)
                timer = null
            }
            return debounceFn
        }

        function ajax(e, arg1, arg2) {
            console.log('ajaxajaxajax');
            // Arguments(3) [InputEvent, 100, 200, callee: ƒ, Symbol(Symbol.iterator): ƒ]
            console.log(arguments);
            console.log(this);  //<input type="text" class="search">
            return 100
        }

        var search = document.querySelector('.search')
        var debounceWrap = debounce(ajax, 1000, true)


        search.addEventListener('input', function () {
            debounceWrap.apply(this, [event, 100, 200]).then(res => {
                console.log(res);   //100
            })
        })

        var cancel = document.querySelector('.cancel')

        cancel.addEventListener('click', function () {
            debounceWrap.cancel()
        })


    </script>
</body>

throttle

definition

Initial implementation

<body>
    <input type="text" class="search">
    <script>
        function throttle(fn, interval) {
            var lastTime = 0
            return function () {
                var _this = this
                var _arguments = arguments
                var newTime = new Date().getTime()

                if (newTime - lastTime > interval) {
                    fn.apply(_this, _arguments)
                    lastTime = newTime
                }
            }
        }

        // Get input box
        var search = document.querySelector('.search');

        // Listening events
        var counter = 0;
        function searchFunc(event) {
            counter++;
            console.log("send out" + counter + "Secondary network request");
            console.log(this);
        };

        search.addEventListener('input', throttle(searchFunc, 1000))


    </script>
</body>

Implementation 1: optimize the last execution

<body>
    <input type="text" class="search">
    <script>
        function throttle(fn, interval, trailing) {
            var lastTime = 0
            var timer
            var trailing = trailing || false
            return function () {
                var _this = this
                var _arguments = arguments
                var newTime = new Date().getTime()

                clearTimeout(timer)

                if (newTime - lastTime > interval) {
                    fn.apply(_this, _arguments)
                    lastTime = newTime
                } else if (trailing) {
                    timer = setTimeout(() => {
                        fn.apply(_this, _arguments)
                    }, interval);
                }
            }
        }

        // Get input box
        var search = document.querySelector('.search');

        // Listening events
        var counter = 0;
        function searchFunc(event) {
            counter++;
            console.log("send out" + counter + "Secondary network request");
            console.log(this);
        };

        search.addEventListener('input', throttle(searchFunc, 1000, true))


    </script>
</body>

Implementation 2: optimize the return value (callback version)

<body>
    <input type="text" class="search">
    <script>
        function throttle(fn, interval, option) {
            var lastTime = 0
            var timer
            var option = option || {}
            var trailing = option.trailing || false
            var callback = option.callback || null
            return function () {
                var _this = this
                var _arguments = arguments
                // Get the current latest timestamp
                var newTime = new Date().getTime()

                // Clear the timer whenever an event is triggered
                if (timer) {
                    clearTimeout(timer)
                }

                var result
                if (newTime - lastTime > interval) {
                    result = fn.apply(_this, _arguments)
                    if (callback) {
                        callback(result)
                    }

                    lastTime = newTime
                } else if (trailing) {
                    timer = setTimeout(() => {
                        result = fn.apply(_this, _arguments)
                        if (callback) {
                            callback(result)
                        }
                    }, interval);
                }
            }
        }

        // Get input box
        var search = document.querySelector('.search');

        // Listening events
        var counter = 0;
        function searchFunc(event) {
            counter++;
            console.log("send out" + counter + "Secondary network request");
            console.log(this);
            return 100
        };

        search.addEventListener('input', throttle(searchFunc, 1000, {
            trailing: true,
            callback: function (res) {
                console.log(res);   // 100
            }
        }))

        // var funWrap = throttle(searchFunc, 1000, {
        //     trailing: true,
        //     callback: function (res) {
        //         console.log(res);
        //         return res
        //     }
        // })

        // search.addEventListener('input', function () {
        //     funWrap.call(this)
        // })


    </script>
</body>

Implementation 3: optimize the return value (Promise version)

<body>
    <input type="text" class="search">
    <script>
        function throttle(fn, interval, option) {
            var lastTime = 0
            var timer
            var option = option || {}
            var trailing = option.trailing || false
            return function () {
                var _this = this
                var _arguments = arguments
                // Get the current latest timestamp
                var newTime = new Date().getTime()

                // Clear the timer whenever an event is triggered
                if (timer) {
                    clearTimeout(timer)
                }

                var result
                return new Promise((resolve, reject) => {
                    if (newTime - lastTime > interval) {
                        result = fn.apply(_this, _arguments)
                        resolve(result)

                        lastTime = newTime
                    } else if (trailing) {
                        timer = setTimeout(() => {
                            result = fn.apply(_this, _arguments)
                            resolve(result)
                        }, interval);
                    }
                })
            }
        }

        // Get input box
        var search = document.querySelector('.search');

        // Listening events
        var counter = 0;
        function searchFunc(event) {
            counter++;
            console.log("send out" + counter + "Secondary network request");
            console.log(this);
            return 100
        };

        // search.addEventListener('input', throttle(searchFunc, 1000, {
        //     trailing: true,
        // }))

        var funWrap = throttle(searchFunc, 1000, {
            trailing: true,
        })

        search.addEventListener('input', function () {
            funWrap.call(this).then(res => {
                console.log(res);
            })
        })

    </script>
</body>

Throttle uncommented version

<body>
    <input type="text" class="search">
    <script>
        function throttle(fn, interval, option) {
            var lastTime = 0
            var timer
            var option = option || {}
            var trailing = option.trailing || false
            return function () {
                var _this = this
                var _arguments = arguments
                var newTime = new Date().getTime()

                if (timer) {
                    clearTimeout(timer)
                }

                var result
                return new Promise((resolve, reject) => {
                    if (newTime - lastTime > interval) {
                        result = fn.apply(_this, _arguments)
                        resolve(result)

                        lastTime = newTime
                    } else if (trailing) {
                        timer = setTimeout(() => {
                            result = fn.apply(_this, _arguments)
                            resolve(result)
                        }, interval);
                    }
                })
            }
        }

        // Get input box
        var search = document.querySelector('.search');

        // Listening events
        var counter = 0;
        function searchFunc(event) {
            counter++;
            console.log("send out" + counter + "Secondary network request");
            console.log(this);
            return 100
        };

        // search.addEventListener('input', throttle(searchFunc, 1000, {
        //     trailing: true,
        // }))

        var funWrap = throttle(searchFunc, 1000, {
            trailing: true,
        })

        search.addEventListener('input', function () {
            funWrap.call(this).then(res => {
                console.log(res);
            })
        })


    </script>
</body>

Added by Ygrek on Fri, 25 Feb 2022 13:42:00 +0200