有關requestAnimationFrame

今晚的雨停了發表於2019-05-05

是什麼

我們先來看一下JS的Event Loop都幹了些什麼:

EventLoop執行過程:

  1. 執行同步程式碼
  2. 執行當前佇列尾部所有微任務
  3. 必要的話渲染UI(瀏覽器是60hz重新整理率,所以16ms一幀更新一次UI)
    1. resize/scroll事件(16Ms一次,自帶節流)
    2. 判斷是否觸發media query
    3. 更新動畫傳送事件
    4. 全屏操作事件
    5. 執行requestAnimationFrame回撥
    6. 執行intersectionObserver回撥
    7. 更新UI
    8. 如果還有時間,自行requestldleCallback

瀏覽器重繪頻率一般會和顯示器的重新整理率保持同步。比如顯示器螢幕重新整理率為 60Hz,使用requestAnimationFrame API,那麼回撥函式就每1000ms / 60 ≈ 16.7ms執行一次。

requestAnimationFrame 會把每一幀中的所有 DOM 操作集中起來,在一次重繪或迴流中就完成,並且重繪或迴流的時間間隔緊緊跟隨瀏覽器的重新整理頻率。通過定時器 setTimeout 或者 setInterval實現動畫。但是定時器動畫第一是動畫的循時間環間隔不好確定,設定長了動畫顯得不夠平滑流暢,設定短了瀏覽器的重繪頻率會達到瓶頸,第二個問題是定時器第二個時間引數只是指定了多久後將動畫任務新增到瀏覽器的UI執行緒佇列中,如果UI執行緒處於忙碌狀態,那麼動畫不會立刻執行。為了解決這些問題,H5 中加入了 requestAnimationFrame。

效能

談到效能,我們再回到上文的Event Loop。當你開啟一個 瀏覽器Tab 頁時,其實就是建立了一個程式,一個程式中可以有多個執行緒,比如渲染執行緒、JS 引擎執行緒、HTTP 請求執行緒等等。當你發起一個請求時,其實就是建立了一個執行緒,當請求結束後,該執行緒可能就會被銷燬。 JS 執行的時候可能會阻止 UI 渲染,這說明了兩個執行緒是互斥的。

所以在低端機上setTimeout偶爾卡頓,是因為它是需要等待主執行緒程式碼執行的。如果佇列前面已經加入了其他任務,那動畫程式碼就要等前面的任務完成後再新增到【瀏覽器 UI 執行緒佇列】。而且重新整理頻率受螢幕解析度和螢幕尺寸影響,不同裝置的螢幕重新整理率可能不同,setTimeout只能設定固定的時間間隔,這個時間和螢幕重新整理間隔可能不同。這都會引起執行步調和螢幕的重新整理步調不一致,引起丟幀。

另外,當頁面處於未啟用的狀態下requestAnimationFrame也是暫停執行的,這也會改進效能。

重複繪製

多次呼叫帶有同一回撥函式的 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 的佇列裡,同樣的回撥函式只有一個。示意程式碼如下:

const onScroll = e => {
    if (framing) return
    let framing = true
    window.requestAnimationFrame(timestamp => {
        framing = false
        animation(timestamp)
    })
}
window.addEventListener('scroll', onScroll)
複製程式碼

資料渲染

參見經典面試題:‘如何渲染幾萬條資料並不卡住介面’

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);
複製程式碼

相關文章