赞
踩
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
注意了:如果n秒内又被触发,则重新计时。
也就是说,他是通过“ 清空setTimeout并重新计算 ”的方式运行的。
//防抖
let timer=null;
$(window).scroll(()=>{
if(timer){
clearTimeout(timer);
}
timer=setTimeout(()=>{
//延时200ms,处理滚动逻辑
},200);
})
这是我在小程序页面滚动中写的一段代码,他就完美实现了 停下之后再执行 这样一个解决程序因为监听严重耗费性能的功能。
适用场景:
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)
}
}
那什么是 节流 ?
节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有(第)一次生效。
这个就有意思了:在一定时间内,如果触发过,就不再执行相同代码段!
//节流
let canRun=true;
window.addEventListener('resize',()=>{
if(!canRun){
return;
}
canRun=false;
setTimeout(()=>{
canRun=true;
//一些好玩的事情
},300);
});
这段代码是我在所做的网站中实现的“监听网页宽度变化实现某些响应式功能”的功能。(当然,这里只是简化版的。具体实现的太长了)
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); } } }
这其中就涉及到setTimeout的执行机制了——JS单线程机制与异步编程!
也就是常说的 setTimeout是否按时结束?
其实,js中存在一个 事件循环机制 ,因为主线程一直有任务,直到setTimeout(fn,n)的n毫秒后,主线程才会放弃任务(或完成),然后立即去执行macrotask中的setTimeout回调任务。(如下图)
js中还有一个 队列机制 :比如你执行setTimeout(task,100)
,其实只是确保这个任务,会在100ms后进入macrotask队列,但并不意味着他能立刻执行,可能当前主线程正在进行一个耗时的操作,也可能目前macrotask队列有很多任务,所以用setTimeout作为倒计时其实并不会保证准确。
定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到、并执行。
而且,有一个setTimeout,就会进入一个队列,再进来,就会继续去“排队”,所以在上面的代码中,在前一次的setTimeout的300ms内,再变化也不会去执行setTimeout里面的事情。也就造成了“canRun为false”->“一定时间内只生效一次”的效果。
适用场景:
事件循环机制: 主线程运行时,产生堆和栈,栈中的代码调用各种外部API,异步操作执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取消息队列,依次执行那些异步任务所对应的回调函数。
即,主线程不断的重复获得消息、之星消息、再取消息、再执行——故称之为“事件循环”!
JS执行是单线程的,它基于“事件循环”。JS执行的流程大概是这样的:
而上面标注为蓝色感叹号的,就是我们要说的“事件循环”!,还有“宏任务”和“微任务”两个概念。我们详细说明下:
resize
、scroll
、帧动画回调requestAnimationFrame
、IntersectionObserver
、重新绘制用户界面requestIdleCallback
回调宏任务,称为(macro )task
。它的作用是为了让浏览器能够从内部获取JavaScript/Dom的内容并确保执行栈能够顺序进行。
宏任务的调度是随处可见的,例如解析HTML、获得鼠标点击的事件回调等。
Macrotask常见任务:
微任务,称为micro task
。通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为作出反应、或者新增一些异步任务,而不需要新建一个宏任务队列。
只要执行栈没有其他的JavaScript在执行,在每个宏任务结束时,微任务队列就会在回调后处理
Microtask常见任务:
这里需要注意一点:“任务队列”是针对“异步任务”来说的,在promise中,
new Promise()
是同步任务,then才是异步任务!
实践可得:微任务具有“高优先级”特性 —— 这是为了确保队列中的微任务在一次事件循环前被执行完毕。
所以在“同层级”下,微任务比宏任务先执行!
但大多文章都说第一步应该是“先取出队列中第一个宏任务并执行”。这让我很疑惑?有理解的前辈请在评论中教我!
// 3.清空微任务队列后开始执行第一个宏任务 setTimeout(()=>{ console.log('1'); // 4.新的宏任务,先放一边 setTimeout(()=>{ console.log('1-1') }) // 5.继续清空微任务队列,这一步过后按序执行2、4 Promise.resolve().then(()=>{ console.log('1-2') }) }) // 1.先执行微任务 Promise.resolve().then(()=>{ console.log(2) // 2.产生新的宏任务,先放一边 setTimeout(()=>{ console.log(3) }) })
vue官网是这样描述的:
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
// 源码 import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false const callbacks = [] let pending = false /** * 对所有callback进行遍历,然后指向响应的回调函数 * 使用 callbacks 保证了可以在同一个tick内执行多次 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。 */ 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 // 此时便会触发回调 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } } // 该函数的作用就是延迟 cb 到当前调用栈执行完成之后执行 export function nextTick (cb?: Function, ctx?: Object) { // 传入的回调函数会在callbacks中存起来 let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次 if (!pending) { pending = true timerFunc() } // 当nextTick不传参数的时候,提供一个Promise化的调用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
其中 timerFunc()
算是比较重要的了:它是根据当前环境判断使用哪种方式实现,按照 Promise.then
和 MutationObserver
以及setImmediate
的优先级来判断,支持哪个就用哪个,如果执行环境不支持,就会降级为 setTimeout 0
,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。
简单来说:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。