Anti shake and throttling
Shake proof
Anti shake function principle : When the event is triggered n Seconds before the callback , If in this n It's triggered in seconds , Then the time will be counted again . Pay attention : If n It's triggered in seconds , Then the time will be counted again . in other words , He is through “ Empty setTimeout And recalculate ” The way it works .
// Shake proof
let timer=null;
$(window).scroll(()=>{
if(timer){
clearTimeout(timer);
}
timer=setTimeout(()=>{
// Time delay 200ms, Handle scrolling logic
},200);
})
Copy code
This is a piece of code I wrote in the scrolling of the applet page , He made it perfect Stop and then execute Such a solution is due to the monitoring of functions that are seriously performance consuming . Applicable scenario :
- Button submit scenario : Prevent multiple submissions button , Perform only the last commit
- Server verification scenario : Form validation needs server cooperation , Only the last time a continuous input event is executed , And search association words have similar functions
Handwritten version of interview :
function debounce(fn, delay, args, context) {
let timer = null;
return function() {
context = context || this;
args = args || arguments;
if(timer != null) {
clearTimeout(timer);
}
timer = setTimeout(function() {
fn.apply(context, args);
}, delay)
}
}
Copy code
throttle
What is throttle ? Principle of throttling function : Within one unit time , Function can only be triggered once . If multiple functions are triggered in this unit time , Only ( The first ) One time . This is interesting : In a certain amount of time , If triggered , No longer execute the same code snippet !
// throttle
let canRun=true;
window.addEventListener('resize',()=>{
if(!canRun){
return;
}
canRun=false;
setTimeout(()=>{
canRun=true;
// Something fun
},300);
});
Copy code
This code is implemented in my website “ Monitor web page width changes to achieve some responsive functions ” The function of .( Of course , Here is a simplified version of . The concrete implementation is too long )
Handwritten version of interview
function throttle(fn, delay, args, context) {
let timer = null;
return function() {
context = context || this;
args = args || arguments;
let flag = !timer;
if(!timer) {
timer = setTimeout(function() {
timer = null;
}, delay)
}
if(flag) {
fn.apply(context, args);
}
}
}
Copy code
This involves setTimeout The implementation mechanism of ——JS Single thread mechanism and asynchronous programming !
That's what they say setTimeout Whether to finish on time ? Actually ,js One in Event cycle mechanism , Because the main thread always has tasks , until setTimeout(fn,n) Of n Milliseconds later , The main thread will give up the task ( Or finish ), Then immediately execute macrotask Medium setTimeout Callback task .( Here's the picture )
js One more Queuing mechanism : Like you do setTimeout(task,100)
, It's just to make sure that the task , Will be in 100ms Back entry macrotask queue , But that doesn't mean he can do it right away , It is possible that a time-consuming operation is in progress on the main thread , Or at the moment macrotask There are many tasks in the queue , So use setTimeout As a countdown, it doesn't guarantee accuracy .
Time interval specified by timer , Indicates when to add the timer code to the message queue , Not when to execute the code . So there's no guarantee when the code will actually execute , Depends on when it is picked up by the main thread's event loop 、 And implement .
and , There is one setTimeout, Will enter a queue , Come in again , Will continue to “ line up ”, So in the code above , In the last setTimeout Of 300ms Inside , No more changes will be implemented setTimeout What's inside . That's why “canRun by false”->“ It only takes effect once in a certain period of time ” The effect of .
Applicable scenario :
- Drag scene : Only once in a fixed time , Prevent UHF trigger position change
- Zoom scene : Monitor browser resize
- Animated scene : Avoid triggering animation multiple times in a short time to cause performance problems
Event cycle mechanism : Main thread runtime , Generate heaps and stacks , The code in the stack calls all kinds of external API, After the asynchronous operation is completed , Just in the message queue . As long as the code in the stack is executed , The main thread will read the message queue , Execute the callback functions corresponding to those asynchronous tasks in turn . namely , The main thread repeatedly gets messages 、 Star News 、 Message again 、 Re execution —— So called “ The event loop ”!
The difference between anti shake and throttling ( Icon ):
EventLoop( The event loop ) Detailed explanation
JS Execution is single threaded , It's based on “ The event loop ”.JS The process of execution is like this :
- All synchronization tasks are performed on the main thread , Form an execution stack ;
- Outside the main thread , There is also a “ Task queue ”. As long as asynchronous tasks have run results , It's just “ Task queue ” Put an event in ;
- once “ Execution stack ” All synchronization tasks in are completed , The system will read “ Task queue ”, Look at the events in there . The corresponding asynchronous tasks then end the wait state , Enter the execution stack , Start execution ;
- The main thread repeats the third step .!
And those marked with blue exclamation marks , That's what we're talking about “ The event loop ”!, also “ Macro task ” and “ Micro task ” Two concepts . Let's elaborate on :
- ( Synchronization code execution completed ) Do the micro task first : Check the micro task queue , Execute and empty the micro task queue , If a new micro task is added in this step , Will also be executed together ;
- Take a macro task from the task queue and execute
- Enter the update rendering phase , Determine whether rendering is required ( Note here : Not every round eventloop Will correspond to a browser rendering , According to the screen refresh rate 、 Page performance 、 Whether the page is running in the background, etc , But usually this rendering interval is fixed ( So there are multiple task It may be when a rendering is finished ))
- For documents that need to be rendered , Perform listening
resize
、scroll
、 Frame animation callbackrequestAnimationFrame
、IntersectionObserver
、 Redraw the user interface - Judge whether the current macro task and micro task queue are empty , if , Is to Idle Idle cycle algorithm , Decide whether to execute
requestIdleCallback
Callback - Cycle these steps back and forth
What is a macro task ?
Macro task , be called (macro )task
. Its purpose is to enable the browser to obtain JavaScript/Dom And ensure that the execution stack can proceed in sequence . Macro task scheduling can be seen everywhere , For example, parsing HTML、 Get the event callback of mouse click, etc .
Macrotask Common tasks :
- setTimeout
- setInterval
- setImmediate
- I/O
- User interaction ,UI Rendering
What is micro task ?
Micro task , be called micro task
. Usually used for things that happen directly after the currently executing script , For example, respond to a series of behaviors 、 Or add some asynchronous tasks , Instead of creating a new macro task queue . As long as the execution stack has no other JavaScript In execution , At the end of each macro task , The micro task queue will be processed after the callback
Microtask Common tasks :
- Promise(=> then、async、await)
- MutationObserver
- process.nextTick(node.js)
Here's a little bit of attention :“ Task queue ” Is aimed at “ Asynchronous task ” Speaking of , stay promise in ,
new Promise()
It's a synchronization task ,then It's the asynchronous task !
Practice leads to : Micro tasks have “ High priority ” characteristic —— This is to ensure that the micro tasks in the queue are executed before an event loop . So in “ At the same level ” Next , Micro tasks execute before macro tasks ! But most articles say that the first step should be “ First take out the first macro task in the queue and execute ”. It makes me wonder ? If you understand, please teach me in the comments !
// 3. After emptying the micro task queue, start executing the first macro task
setTimeout(()=>{
console.log('1');
// 4. New macro task , Put it aside
setTimeout(()=>{
console.log('1-1')
})
// 5. Continue to empty the micro task queue , After this step, execute in sequence 2、4
Promise.resolve().then(()=>{
console.log('1-2')
})
})
// 1. Do the micro task first
Promise.resolve().then(()=>{
console.log(2)
// 2. Generate a new macro task , Put it aside
setTimeout(()=>{
console.log(3)
})
})
Copy code
eventloop practice :$nextTick
vue This is how the official website describes :
Vue Updating DOM Time is executed asynchronously . Just listen for data changes ,Vue A queue will be opened , And buffer all data changes that occur in the same event cycle . If the same watcher Triggered multiple times , Will only be pushed into the queue once . This de duplication of data in buffering avoids unnecessary computation and DOM Operation is very important . then , In the next event loop “tick” in ,Vue Refresh the queue and execute the actual ( It's gone heavy ) Work .
// Source code
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
/** * For all callback Traversal , Then point to the callback function of the response * Use callbacks Guaranteed to be in the same one tick Internal execution multiple times nextTick, Multiple asynchronous tasks will not be opened , And these asynchronous tasks are all pressed into a synchronous task , The next tick completion of enforcement . */
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]( "i")
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
// This will trigger the callback
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// The function is to delay cb Executes after the execution of the current call stack
export function nextTick (cb?: Function, ctx?: Object) {
// The callback function passed in will be in callbacks Save it
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// pending It's a status tag , Guarantee timerFunc The next tick Only once before
if (!pending) {
pending = true
timerFunc()
}
// When nextTick When you don't transfer parameters , Provide a Promise The call of
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
Copy code
among timerFunc()
It's more important : It is based on the current environment to determine which way to implement , according to Promise.then
and MutationObserver
as well as setImmediate
To determine the priority of , You can use whatever you support , If the execution environment does not support , Will be downgraded to setTimeout 0
, Even though it has execution delays , May cause multiple renderings , There is no way .
Simply speaking :
- vue Using asynchronous queues to control DOM Update and nextTick Callbacks are executed one after another
- microtask Because of its high priority feature , It can ensure that the micro tasks in the queue are completed and prioritized before an event cycle
- Because of compatibility issues ,vue Had to do microtask towards macrotask The downgrade of