一,浏览器渲染原理
1,页面渲染的流程
当浏览器拿到资源后,做了啥才将页面呈现给用户?
2,布局(回流)与绘制(重绘)
第一次布局叫布局,后续DOM操作等导致的布局叫回流。
影响回流的操作:
可以打开rendering查看情况:绿色的部分就是发生了重绘的部分:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style type="text/css">
.test{
width: 200px;
height: 200px;
background-color: red;
}
</style>
</head>
<body>
<div class="test"></div>
<div class="ulbox">
<ul class="oul">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
<li>第四个</li>
</ul>
<button id="btn">增加一个li(回流加重绘)</button>
<button id="btn2">改变背景色(仅仅重绘)</button>
</div>
<script type="text/javascript">
var oul=document.getElementsByClassName('oul')[0]
var test=document.getElementsByClassName('test')[0]
var btn=document.getElementById('btn')
var btn2=document.getElementById('btn2')
btn.onclick=function(e){
var newP = document.createElement("li");
var textNode = document.createTextNode("新的节点");
newP.appendChild(textNode);
oul.appendChild(newP)
}
btn2.onclick=function(e){
if(test.style.backgroundColor=='red'){
test.style.backgroundColor='green'
}else{
test.style.backgroundColor='red'
}
}
</script>
</body>
</html>
然后再看持续性地强制回流:
const update=(timerstamp)=>{
test.style.width=((Math.sin(test.offsetTop+timerstamp/1000)+1)*500)+'px'
window.requestAnimationFrame(update)
}
window.addEventListener('load',update)
这个动画,一直读取toffsetop的值,就会导致回流一直在发生。
而避免回流和重绘可以采用一个插件:fastdom
https://github.com/wilsonpage/fastdom
将读与写区分开来进行操作了。
const update=(timerstamp)=>{
//通过fastdom.measure进行读操作
fastdom.measure(()=>{
var top=test.offsetTop
//利用fastdom.mutate进行写操作
fastdom.mutate(()=>{
test.style.width=((Math.sin(top+timerstamp/1000)+1)*500)+'px'
})
})
window.requestAnimationFrame(update)
}
window.addEventListener('load',update)
3,复合线程(compositor thread)与图层
为了提高绘制的效率,渲染器采取了复合的手段,相当于ps中的图层,每一图层绘制一部分内容,当所有图层绘制完毕,组合到一起,就是我们的页面绘制完成。
这样,有的时候,我们页面的元素发生变化,就可以只让他影响某一个图层发生变化,而不是全部图层发生变化。这样一来,我们的绘制工作就可以更加高效地完成。
拆分图层的依据:会相互影响的发放置到同一个图层。
那如何查看图层:ctrl+shift+p,搜索打开layers:
4,只触发 复合,但不会触发回流和重绘的几个属性
所以,动画的制作用这个比改变top,left等值要来得性能高。
但是们也不应该全部都创建成一个新的图层,因为图层太多了,开销也太大。
可以对比一下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
.box{
width: 1300px;
height: 1000px;
margin: 0 auto;
}
.oul{
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
list-style: none;
}
.oli{
flex-shrink: 0;
margin: 10px;
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
background-color: yellow;
transition: all 1s linear;
}
.oli:hover .odiv{
width: 150px;
height: 150px;
}
.odiv{
width: 100px;
height: 100px;
background-color: red;
}
</style>
<body>
<div class="box">
<ul class="oul">
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
</ul>
</div>
</body>
</html>
这种改变宽高的部分会引起回流和重绘:
然后改写代码为tranform,这种css3的动画,是只影响复合,并不会引发回流和重绘:
.oli:hover .odiv{
transform: scale(1.5);
transform:rotate(45deg);
}
因为上个我不懂看。。。来自菜鸡的悲伤!!!!所以机智的我就又写了一个:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
.box{
width: 1300px;
height: 300pxpx;
margin: 0 auto;
}
.oul{
width: 100%;
height: 200px;
display: flex;
justify-content: space-between;
list-style: none;
}
.oli{
flex-shrink: 0;
margin: 10px;
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
background-color: yellow;
}
.active{
animation: move 3s infinite linear;
}
.odiv{
width: 100px;
height: 100px;
background-color: red;
}
.btn{
display: block;
margin: 0 auto;
}
@keyframes move{
0%{transform: rotate(0deg);}
100%{transform: rotate(360deg);}
}
</style>
<body>
<div class="box">
<ul class="oul">
<li class="oli" ><div class="odiv" id="thirst"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
<li class="oli"><div class="odiv"></div></li>
</ul>
</div>
<button class="btn">按钮</button>
<script type="text/javascript">
var firstodiv =document.getElementById('thirst')
var btn=document.getElementsByClassName("btn")[0]
btn.onclick=function(){
firstodiv.classList.add('active')
}
</script>
</body>
</html>
点击按钮后,第一个方块会不断旋转:
先查看无操作的时候的performance:主线程没有任何回流重绘:
而触发tranform动画之后,我看了下,只有动画开始的时候会产生回流和重绘:
作为对比,我把这个动画改成宽高改变的:
@keyframes move{
0%{
width: 100px;
height: 100px;
}
50%{
width: 150px;
height: 150px;
}
100%{
width: 100px;
height: 100px;
}
}
现在来看就很明显了:
图中的每一个波峰都进行了回流和重绘的操作!主线程一直在间隙地被占用!!!
终于看懂了!流下了木有技术地泪水!!!!!呜呜呜!!
更加直观的观察方式是打开观察重绘的选项:至于怎么打开:可以按ctrl+shift+p然后搜索rendering就可以看到了:
第一种情况使用tranform,仅仅动画开始的一瞬间变成绿色(引发重绘):
第二种情况,必然是绿色的框一直存在,代表着一直在重绘:
所以,我们做动画的时候,尽量用transform
当然,要提取到新的图层,还需要:
will-change: 'transform';
二,高频事件防抖
如滚动,鼠标滑动等事件,都算高频事件。如果任务量太重,没有办法在16ms内完成一帧,就会产生抖动、卡顿的问题。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
.box{
width: 1300px;
height: 1000pxpx;
margin: 0 auto;
}
.oul{
width: 100%;
height: 200px;
display: flex;
justify-content: space-between;
list-style: none;
}
.oli{
flex-shrink: 0;
margin: 10px;
width: 200px;
height: 200px;
list-style: none;
background-color: yellow;
}
</style>
<body>
<div class="box">
<ul class="oul">
<li class="oli" ></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
</ul>
</div>
<script type="text/javascript">
var lis=document.getElementsByClassName("oli")
function changeWith(rand){
for(let i=0;i<lis.length;i++){
lis[i].style.width=(Math.sin(rand/1000)+1)*100+'px'
}
}
window.addEventListener('mousemove',(e)=>{
console.log("aa")
let pos=e.clientX
changeWith(pos)
})
</script>
</body>
</html>
查看状态:一直在进行回流和重绘:
这就导致实际的场景中,动画会变得卡顿。有一个函数requestAnimationFrame可以帮助我们解决这个问题。
但是讲这个函数之前,需要先了解下每产生一帧动画所做的事情;
1,RAF时间调度
使用RAF的方法,把要处理的事情放在这里面就可以了:
window.addEventListener('mousemove',(e)=>{
let pos=e.clientX
window.requestAnimationFrame(()=>{
changeWith(pos)
})
})
2,防抖:一段时间内没触发,才可以执行下一次
原理:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
防抖有一个问题:那就是如果事件一直在触发,那么执行函数永远都得不到执行。这种情况下,函数节流此时是较好的方法。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<style type="text/css">
.box{
width: 1300px;
height: 1000pxpx;
margin: 0 auto;
}
.oul{
width: 100%;
height: 200px;
display: flex;
justify-content: space-between;
list-style: none;
}
.oli{
flex-shrink: 0;
margin: 10px;
width: 200px;
height: 200px;
list-style: none;
background-color: yellow;
}
</style>
<body>
<div class="box">
<ul class="oul">
<li class="oli" ></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
<li class="oli"></li>
</ul>
</div>
<script type="text/javascript">
var lis=document.getElementsByClassName("oli")
function changeWith(rand){
for(let i=0;i<lis.length;i++){
lis[i].style.width=(Math.sin(rand/1000)+1)*100+'px'
}
}
let ticking=false
window.addEventListener('mousemove',(e)=>{
let pos=e.clientX
//若事情还在执行,则是true,不执行事件
if(ticking) return
//改成true,事情开始执行
ticking=true
window.requestAnimationFrame(()=>{
changeWith(pos)
//执行完毕,又可以变成false(又可以执行事情了)
ticking=false
})
})
</script>
</body>
</html>
这样处理之后,也就是只有本次事情处理完毕之后才能开始处理下一个事情(其实我感觉这特么是节流啊!!!!视频里面说这是防抖???)。也就是鼠标的移动事件不再根据鼠标移动来触发。而是根据raf来调度触发。保证每一秒都可以生成60帧的动画。
三,react的时间调度实现
底层的原理就是这个RAF