Strange Problems and Solutions of JavaScript Anti shake and Throttling
- 2021-09-24 21:09:56
- OfStack
Scene
There are already a lot of articles about anti-shake and throttling on the Internet. Why should we write another one? As a matter of fact, anti-shake and throttling, we found some strange problems in use, and after several modifications, here we mainly share the problems we encountered and how to solve them.
Why use anti-shake and throttling?
Because some functions are triggered/called too quickly, we need to manually limit the frequency of their execution. For example, the common event of listening to scroll bars, if there is no anti-shake processing, and if the time spent on each function execution exceeds the interval between triggers, the page will jam.
Evolution
Initial implementation
We first implement a simple dejitter function
function debounce(delay, action) {
let tId
return function(...args) {
if (tId) clearTimeout(tId)
tId = setTimeout(() => {
action(...args)
}, delay)
}
}
Test 1
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
Well, it seems that the basic effect has been realized. The wrapped function fn is called twice, but it is not executed immediately, but waits for the time interval to pass before finally executing once.
What about this?
However, the above implementation has a fatal problem and does not handle this! You may not feel it when you use it for native event handling, but when you use this-sensitive code such as ES6 class, you will definitely encounter problems caused by this.
For example, the following uses class to declare 1 counter
class Counter {
constructor() {
this.i = 0
}
add() {
this.i++
}
}
We may want to add a new attribute fn to constructor
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add)
}
add() {
this.i++
}
}
Unfortunately, the this binding here is problematic. Try the following code
const counter = new Counter()
counter.fn() // Cannot read property 'i' of undefined
An exception is thrown
Cannot read property 'i' of undefined
The reason is that
this
Without binding, we can bind manually
this .bind(this)
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add.bind(this))
}
add() {
this.i++
}
}
But a better way is to modify
debounce
That enables it to bind automatically
this
function debounce(delay, action) {
let tId
return function(...args) {
if (tId) clearTimeout(tId)
tId = setTimeout(() => {
action.apply(this, args)
}, delay)
}
}
Then, the code will run as expected
;(async () => {
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add)
}
add() {
this.i++
}
}
const counter = new Counter()
counter.add()
counter.add()
console.log(counter.i) // 2
counter.fn()
counter.fn()
console.log(counter.i) // 2
await wait(20)
console.log(counter.i) // 3
})()
What about the return value?
I don't know if you have found out, but use it now
debounce
Wrapped functions have no return value, which is a function with only side effects. However, we still encounter scenarios where we need to return values.
For example, after input stops, use Ajax to request background data to determine whether the same data already exists.
Modify
debounce
An implementation that caches the results of the last execution and has initial result parameters
function debounce(delay, action, init = undefined) {
let flag
let result = init
return function(...args) {
if (flag) clearTimeout(flag)
flag = setTimeout(() => {
result = action.apply(this, args)
}, delay)
return result
}
}
The calling code becomes
;(async () => {
class Counter {
constructor() {
this.i = 0
this.fn = debounce(10, this.add, 0)
}
add() {
return ++this.i
}
}
const counter = new Counter()
console.log(counter.add()) // 1
console.log(counter.add()) // 2
console.log(counter.fn()) // 0
console.log(counter.fn()) // 0
await wait(20)
console.log(counter.fn()) // 3
})()
It looks perfect? However, not considering asynchronous functions is a big failure!
Try the following test code
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
0
An exception is thrown
fn(...).then is not a function
Because our wrapped function is synchronized, the value returned the first time is not of type Promise.
Unless we change the default value
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
1
Support asynchronous functions with return values
There are two ways to support asynchronism
Wrapping asynchronous functions as synchronous functions Make the wrapped function asynchronousThe first idea is realized
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
2
The calling mode and synchronous function are completely 1, of course, it supports asynchronous functions
;(async () => {
const get = async i => i
console.log(await get(1))
console.log(await get(2))
// Note that the default value is changed to Promise
const fn = debounce(10, get, 0)
console.log(fn(3)) // 0
console.log(fn(4)) // 0
await wait(20)
console.log(fn(5)) // 4
})()
The second way of thinking is realized
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
4
Call mode supports asynchronous mode
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
5
As you can see, the problem with the first idea is that the return value will always be the old return value, and the main problem with the second idea is to wrap the synchronous function as asynchronous. Weighing the pros and cons, we think the second idea is more correct. After all, it is unlikely that the usage scenario itself must be a synchronous operation. Also, the original setTimeout is asynchronous, but you didn't realize it when you didn't need to return a value.
Avoid the loss of original function information
Later, someone raised a question. If the function carries other information, such as $like jQuery, it is a function, but it also contains other attributes. If you use debounce, you can't find it.
At first, we immediately thought of all the traversible properties on the copy function, and then thought of the Proxy feature of ES6, which is really magical. Using Proxy to solve this problem will be exceptionally simple because in addition to calling the function, other 1-cut operations still point to the original function!
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
6
Test 1
// Use Promise Simple encapsulation setTimeout , the same below
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
let num = 0
const add = () => ++num
add()
add()
console.log(num) // 2
const fn = debounce(10, add)
fn()
fn()
console.log(num) // 2
await wait(20)
console.log(num) // 3
})()
7
Realize throttling
A throttle function throttle is realized in this way
/**
* Function throttling
* Throttling (throttle) Jean 1 Do not execute a function too frequently, reduce the execution of too fast calls, called throttling
* Function dejitter similar to but different from the above , Wrapped function on 1 The second operation will be executed directly after the minimum interval has passed , Otherwise, the operation will be ignored
* The obvious difference from the above function debounce is that the operation will be performed in a loop with the minimum interval time during continuous operation , Instead of just executing the last 1 Secondary operation
* Note : The function is the first 1 Secondary call 1 Will be implemented, don't worry about the first 1 If you can't get the cached value for the next time, you will get it for subsequent consecutive calls 1 Cached value of times
* Note : Higher-order functions that return function results need to use the {@link Proxy} Implementation to avoid information loss on the prototype chain of the original function
*
* @param {Number} delay Minimum interval time, in units ms
* @param {Function} action Actions that really need to be performed
* @return {Function} A function with throttling function after packaging. This function is asynchronous, and the function that needs to be wrapped {@link action} There is not much correlation between asynchronous and asynchronous
*/
const throttle = (delay, action) => {
let last = 0
let result
return new Proxy(action, {
apply(target, thisArg, args) {
return new Promise(resolve => {
const curr = Date.now()
if (curr - last > delay) {
result = Reflect.apply(target, thisArg, args)
last = curr
resolve(result)
return
}
resolve(result)
})
},
})
}
Summarize
Well, in fact, the anti-shake and throttling here are still simple implementations, and other functions such as canceling anti-shake/forcing cache flushing have not yet been implemented. Of course, the function is enough for our generation, and it has also been put into the public function library rx-util.
The above is JavaScript anti-shake and throttling encountered strange problems and solve the details, more information about JavaScript anti-shake and throttling please pay attention to other related articles on this site!