简介
对于从输入URL到看到界面这个经典的面试题,每个同学都能娓娓道来。其中包括DNS解析、缓存、渲染,而渲染过程又包括DOM和CSS解析、布局树的计算、分层、绘制、分块、光栅化、合成。
当首屏界面渲染完后,在应用的运行过程中,还会继续渲染,那么这个渲染过程是什么样的呢?
首先,浏览器会定时刷新界面,不管js有没有改变dom。通常刷新频率和显示器帧率相同,大部分显示器帧率是60fps,≈16ms每帧。
如果渲染某一帧时候,DOM并未发生改变,就不需要启动主线程计算,如果DOM发生了变化,就可能触发重排或重绘,这时候就会启动主线程,重复上面提到的渲染过程。
在定时刷新帧的间隔,还会有其他的任务。比如用户可能会触发一些元素事件(输入文本点击按钮等),还可能有定时任务或者异步任务执行,这些任务的执行是会阻塞渲染的。
除了上面的任务,还有requestAnimationFrame和requestIdleCallback。在每一帧渲染完成后,在下一帧绘制之前如果有requestAnimationFrame,则调用之。如果在一帧渲染完后有空闲,就会执行requestIdleCallback注册的回调什么是空闲时间呢,以60fps为例,如果浏览器渲染一帧发现不到16ms,那么剩余时间就算是空闲时间。
requestAnimationFrame
通常我们可以通过定时器(setTimeout/setInterval)实现一个动画,但是定时器时间并不精确,如果时间太短,那么可能造成多余的操作,消耗CPU,如果时间长,就会导致动画不流畅。另外当画面不展示时候,定时器依然执行,导致不必要的CPU资源消耗,耗电更快。
requestAnimationFrame实现动画可以解决上面两个问题。
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
使用:
接受一个回调,在下次渲染前执行,注意timestamp
参数,在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳。
该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻,这个时间用来进行动画参数的计算。
参考官方示例
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
由于该方法只是通知浏览器下一次绘制之前执行一次callback,因此实现动画时候需要每次重新调用。
requestAnimationFrame返回一个id,取消回调的方法是cancelAnimationFrame,请看官方示例
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
var start = window.mozAnimationStartTime; // 只有Firefox支持mozAnimationStartTime属性,其他浏览器可以使用Date.now()来替代.
var myReq;
function step(timestamp) {
var progress = timestamp - start;
d.style.left = Math.min(progress/10, 200) + "px";
if (progress < 2000) {
myReq = requestAnimationFrame(step);
}
}
myReq = requestAnimationFrame(step);
window.cancelAnimationFrame(myReq);
需要注意动画中元素样式最好根据时间进行计算,而不是使用需要样式计算的DOM API(例如ele.style.offsetWidth),否则会强制重新渲染。
requestIdleCallback
接受一个回调,回调在浏览器空闲时间执行。注意如果浏览器一直渲染没有空闲,可能就一直执行不到requestIdelCallback注册的回调,因此可以设置一个超时时间,超时之后会执行注册的回调。
取消回调的方法是 cancelIdleCallback
var handle = window.requestIdleCallback(callback, {timeout: 1000});
cancelIdleCallback(handle);
注意,requestIdleCallback用来执行优先级低的任务,由于其执行时机不可控,因此尽量不要执行DOM操作。