咱们先来看一下JS的Event Loop都干了些什么:面试
- 执行同步代码
- 执行当前队列尾部全部微任务
- 必要的话渲染UI(浏览器是60hz刷新率,因此16ms一帧更新一次UI)
- resize/scroll事件(16Ms一次,自带节流)
- 判断是否触发media query
- 更新动画发送事件
- 全屏操做事件
- 执行requestAnimationFrame回调
- 执行intersectionObserver回调
- 更新UI
- 若是还有时间,自行requestldleCallback
浏览器重绘频率通常会和显示器的刷新率保持同步。好比显示器屏幕刷新率为 60Hz,使用requestAnimationFrame API,那么回调函数就每1000ms / 60 ≈ 16.7ms执行一次。浏览器
requestAnimationFrame 会把每一帧中的全部 DOM 操做集中起来,在一次重绘或回流中就完成,而且重绘或回流的时间间隔牢牢跟随浏览器的刷新频率。经过定时器 setTimeout 或者 setInterval实现动画。可是定时器动画第一是动画的循时间环间隔很差肯定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,若是UI线程处于忙碌状态,那么动画不会马上执行。为了解决这些问题,H5 中加入了 requestAnimationFrame。bash
谈到性能,咱们再回到上文的Event Loop。当你打开一个 浏览器Tab 页时,其实就是建立了一个进程,一个进程中能够有多个线程,好比渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是建立了一个线程,当请求结束后,该线程可能就会被销毁。 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。app
因此在低端机上setTimeout偶尔卡顿,是由于它是须要等待主线程代码执行的。若是队列前面已经加入了其余任务,那动画代码就要等前面的任务完成后再添加到【浏览器 UI 线程队列】。并且刷新频率受屏幕分辨率和屏幕尺寸影响,不一样设备的屏幕刷新率可能不一样,setTimeout只能设置固定的时间间隔,这个时间和屏幕刷新间隔可能不一样。这都会引发执行步调和屏幕的刷新步调不一致,引发丢帧。函数
另外,当页面处于未激活的状态下requestAnimationFrame也是暂停执行的,这也会改进性能。oop
屡次调用带有同一回调函数的 requestAnimationFrame,会致使回调在同一帧中执行屡次,也就是说它并无论理回调函数。可能会有性能问题。但咱们也能够利用它,好比写一个旋转速度随着点击次数增长的代码。每次增长的deg都是1,可是在一帧执行多个回调函数,即绘制了屡次动画,旋转愈来愈快。性能
var deg = 0;
var id;
var div = document.getElementById("div");
div.addEventListener('click', function () {
var self = this;
requestAnimationFrame(function change() {
self.style.transform = 'rotate(' + (deg++) + 'deg)';
id = requestAnimationFrame(change);
});
});
document.getElementById('stop').onclick = function () {
cancelAnimationFrame(id);
};
复制代码
也正由于它无论理回调函数,在滚动、这类高触发频率的事件回调里,可能会形成多余的计算和绘制。例如:动画
window.addEventListener('scroll', e => {
window.requestAnimationFrame(stamp => {
animation(stamp)
})
})
复制代码
次方案是使用节流函数。但节流函数是经过时间管理队列的,而 requestAnimationFrame 的触发时间是不固定的。完美的解决方案是经过 requestAnimationFrame 来管理队列,其思路就是保证 requestAnimationFrame 的队列里,一样的回调函数只有一个。示意代码以下:ui
const onScroll = e => {
if (framing) return
let framing = true
window.requestAnimationFrame(timestamp => {
framing = false
animation(timestamp)
})
}
window.addEventListener('scroll', onScroll)
复制代码
参见经典面试题:‘如何渲染几万条数据并不卡住界面’this
var total = 100000;
var size = 100;
var count = total / size;
var done = 0;
var ul = document.getElementById('list');
function addItems() {
var li = null;
var fg = document.createDocumentFragment();
for (var i = 0; i < size; i++) {
li = document.createElement('li');
li.innerText = 'item ' + (done * size + i);
fg.appendChild(li);
}
ul.appendChild(fg);
done++;
if (done < count) {
requestAnimationFrame(addItems);
}
};
requestAnimationFrame(addItems);
复制代码