Description
浏览器渲染
我们知道从用户输入地址到用户看到内容,是有一个过程的,一道经典的面试题。
问:在浏览器输入网址到看到页面经历了哪些过程
- 浏览器根据对应的域名发送DNS请求,获取到对应的IP
- 获取对应的IP后,根据IP协议传输数据,发送给互联网
- 在互联网中路由器根据目标的IP地址,通过复杂的算法找出‘最佳线路’来传输请求
- 找到服务器的网卡通过TCP三次握手建立连接,在第三次握手的时候发送HTTP请求
- 服务器对请求进行分析,然后返回对应的服务器资源
- 浏览器拿到数据后,然后通过TCP四次挥手关闭连接,然后进行渲染
- 对HTML进行渲染,解析出DOM树,对css解析出styls Rules,然后关联二者生成Render Tree
- Layout根据Render Tree进行计算每个节点的信息
- Painting根据计算好的信息绘制页面
渲染解析
从上面的问题我们可以看出,渲染主要是分为三部分
- 构建DOM树 - (Structure)
- 根据规则确认每一个DOM的位置(render DOM) - Layout
- 绘制每一个DOM的位置
注意:但是浏览器不会一直的渲染,因为渲染的开销还是很大的,渲染引擎和js解析引擎互斥的,所以浏览器会合并优化。
浏览器的合并优化
一段同步的代码修改一个元素的属性,浏览器会直接优化到最后一个
box.style.display = "none";
box.style.display = "block";
box.style.display = "none";
所有浏览器并不是一直在渲染的,浏览器会有固定的节奏去渲染页面。
这就会出现一些问题,例如下面的例子
// 我们计划物体先从0px移动到1000px,然后动画回到500px的位置
box.style.transform = 'translateX(1000px)'
box.style.transition = 'transition 1s ease'
box.style.transform = 'translateX(500px)'
但是结果却不是,浏览器会直接的渲染最后一句,把物体移动500px。这里我们就有一个疑问,那么浏览器的渲染到底什么时候执行呢?
主线程和异步队列
我们都知道node.js的event loop,但是浏览器内部也有一个event loop的模式,当主线程空的时候,才会去tasks中推callback到主线程执行。
当主线程为空的时候,异步队列开关开。当我们执行下面的一段代码的时候, 进入死循环,点击后会导致异步队列永远执行,因此不单单主进程,渲染过程也同样被阻塞而无法执行,因此页面无法再选中(因为选中时页面表现有所变化,文字有背景色,鼠标也变成 text),也无法再更换内容。(但鼠标却可以动!)
button.addEventListener('click', () => {
while(true);
})
requestAnimationFrame
是一个特殊的异步任务,只是它注册的方法(callback)不会加入异步队列,而是加入渲染这一边的队列中,它在渲染的三个步骤之前被执行。通常用来处理渲染相关的工作。
有时候我们不是很懂setTimeout和requestAnimationFrame的区别,今天来一个例子来说明他们的区别:
// moveBoxForwardOnePixel: 让元素像右移动一像素
function moveBoxForwardOnePixel(className) {
const node = document.querySelector(`.${className}`)
const rect = node.getBoundingClientRect();
node.style.left = `${rect.left + 1}px`;
}
function asf() {
moveBoxForwardOnePixel('rect1');
requestAnimationFrame(asf)
}
function set() {
moveBoxForwardOnePixel('rect2')
setTimeout(set, 0);
}
asf()
set()
**重点:**通过上面的例子,我们可有看到,同样的方法,setTimeout运行的次数比asf运行的多很多。(模拟过,一段时间内1000/60也比asf执行多了很多)代码地址
- 这是因为setTimeout在每次运行结束时都把自己的callback放入到异步队列中,当主线程空闲的时候,异步队列中callback会被执行,等渲染过程的时候(不是每次的异步队列都会进到渲染循环,就是说并不会执行异步队列就走进渲染开关)异步队列已经执行好多次了(setTimeout的callback已经执行了很多次),所以渲染部分会一次性渲染很多像素,而不是1px。
- 但是requestAnimationFrame只会在渲染过程之前运行,因此严格的按照执行一次渲染一次,所以一次只移动1px,也正是我们想要的效果。
- 如果在低端环境兼容,常规也会写作
setTimeout(callback, 1000 / 60)
来大致模拟 60 fps 的情况,但本质上setTimeout
并不适合用来处理渲染相关的工作。因此和渲染动画相关的,多用requestAnimationFrame
,不会有掉帧的问题(即某一帧没有渲染,下一帧把两次的结果一起渲染了)
其他运用
asf主要是用于渲染,但是其他方面也有使用,例如:我们可以对上面的例子进行实现。
// 我们计划物体先从0px移动到1000px,然后动画回到500px的位置
box.style.transform = 'translateX(1000px)'
box.style.transition = 'transition 1s ease'
box.style.transform = 'translateX(500px)
//上面的这种方式会被浏览器的合并优化掉,所以我们可以使用asf
box.style.transform = 'translateX(1000px)'
requestAnimationFrame(() => {
box.style.transition = 'transition 1s ease'
box.style.transform = 'translateX(500px)'
})
// 当然我们也可以利用重绘和重排来阻断浏览器的合并优化
box.style.transform = "translateX(100px)";
console.log(box.offsetWidth);
box.style.transition = "transform 1s ease";
box.style.transform = "translateX(200px)";
具体例子可以查看asf的运用
参考链接