咱們先來看一下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);
複製代碼