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>