防抖函数:
在一段时间内,强制函数在某段时间内只执行一次,底层原理使用setTimeout实现。
const debounce = (fn, delay: number) => {
// 记录上次的timer
let inDebounce;
return function(...args) {
// 记录调用时的this上下文
const context = this;
// 先清理上次的timer,不管执没执行都清理掉
clearTimeout(inDebounce);
// 重新开启timer
inDebounce = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
};
export default debounce;
使用防抖:
正常resize会在1S内多次调用getWidth, 当我们加入debounce之后,我们会在闭包函数中维护一个inDebounce的timer变量,实际上每次调用getWidth都会调用到闭包的内部函数,如果在1s内连续调用次方法,我们会清除上一次记录的timer。当没有人再调用时。最后一次的timer才会等1s之后执行一次。
在此例中,就算你连续拖动窗口10S,然后暂停,其实最后只会在第11S的时候执行一次打印操作。
const getWidth = debounce(() => {
console.log("resize: "+window.innerWidth)
}, 1000)
window.addEventListener('resize', getWidth)
截流函数
throttle (截流)强制函数以固定的速率执行
function throttle(fn, threshold) {
// 记录上次执行的时间
var last
// 定时器
var timer
// 默认间隔为 250ms
threshold || (threshold = 250)
// 返回的函数,每过 threshold 毫秒就执行一次 fn 函数
return function () {
// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments
var now = +new Date()
// 如果距离上次执行 fn 函数的时间小于 threshold,那么就放弃
// 执行 fn,并重新计时
if (last && now < last + threshold) {
clearTimeout(timer)
// 保证在当前时间区间结束后,再执行一次 fn
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshold)
// 在时间区间的最开始和到达指定间隔的时候执行一次 fn
} else {
last = now
fn.apply(context, args)
}
}
}
使用截流
const getWidth = throttle(() => {
console.log("resize: "+window.innerWidth)
}, 1000)
window.addEventListener('resize', getWidth)
原理是通过上一次和下一次执行的时间间隔,来决定是否保留上一次的timer。
在此例中,你连续拖动窗口10S,然后暂停,其实会每1秒执行一次打印操作。
lodash
注意leading(先执行再防抖/截流) vs trailing(先防抖/截流再执行)
lodash的防抖截流函数很好的支持相关参数:
debounce
_.debounce(func, [wait=0], [options={}])
func (Function): 要防抖动的函数。
[wait=0] (number): 需要延迟的毫秒数。
[options={}] (Object): 选项对象。
[options.leading=false] (boolean): 指定在延迟开始前调用,默认false。
[options.maxWait] (number): 设置 func 允许被延迟的最大值。
[options.trailing=true] (boolean): 指定在延迟结束后调用,默认true。
throttle
_.throttle(func, [wait=0], [options={}])
func (Function): 要节流的函数。
[wait=0] (number): 需要节流的毫秒数。
[options={}] (Object): 选项对象。
[options.leading=true] (boolean): 指定调用在节流开始前,默认true。
[options.trailing=true] (boolean): 指定调用在节流结束后,默认true。
requestAnimationFrame
requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。(requestAnimationFrame要递归调用,每16ms会触发它的回调函数)
- requestAnimationFrame相对于setTimeout,setInterval会节省CPU开销,当页面隐藏,最小化,其它tab时,setTimeout会后台偷偷搞事情,但是requestAnimationFrame会暂停绘制。
var progress = 0;
//回调函数
function render() {
progress += 1; //修改图像的位置
if (progress < 100) {
//在动画没有结束前,递归渲染
window.requestAnimationFrame(render);
}
}
//第一帧渲染
window.requestAnimationFrame(render);
requestIdleCallback
它调用函数时会给函数传两个参数:
- timeRemaining(): 当前帧还剩下多少时间
- didTimeout: 是否超时
它的作用是是在浏览器一帧的剩余空闲时间内执行任务,
即,在一帧结束前且没有别的高优先级任务的情况下,再执行该任务,
也就是说,requestIdleCallback的优先级低.
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
// 任务队列
const tasks = [
() => {
console.log("第一个任务");
},
() => {
console.log("第二个任务");
},
() => {
console.log("第三个任务");
},
];
function myNonEssentialWork (deadline) {
// 如果帧内有富余的时间,或者超时
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
work();
}
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
function work () {
tasks.shift()();
console.log('执行任务');
}
宏任务 (microtask)
可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
宏任务包含:
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
微任务 (macrotask)
可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
微任务包含:
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)
运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
检测一下:
答案是1,2,3,4
整个这一串代码我们所在的层级我们看做一个任务,其中我们先执行同步代码。第一行的 setTimeout 是异步代码,跳过,来到了 new Promise(…) 这一段代码。前面提到过,这种方式是一个构造函数,是一个同步代码,所以执行同步代码里面的函数,即 console.log(1),接下来是一个 then 的异步,跳过。最后一个是一段同步代码 console.log(2)。所以,这一轮中我们知道打印了1, 2两个值。接下来进入下一步,即之前我们跳过的异步的代码。从上往下,第一个是 setTimeout,还有一个是 Promise.then()。setTimeout 是宏任务的异步,Promise.then()是微任务的异步,微任务是优先于宏任务执行的,所以,此时会先跳过 setTimeout 任务,执行两个 Promise.then() 的微任务。所以此时会执行 console.log(3) 函数。最后就只剩下 setTimeout 函数没有执行,所以最后执行 console.log(4)。
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)