一,浏览器渲染原理
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
/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.clientXchangeWith(pos)})</script></body></html>
查看状态:一直在进行回流和重绘:
这就导致实际的场景中,动画会变得卡顿。有一个函数requestAnimationFrame可以帮助我们解决这个问题。
但是讲这个函数之前,需要先了解下每产生一帧动画所做的事情;
1,RAF时间调度
使用RAF的方法,把要处理的事情放在这里面就可以了:
window.addEventListener('mousemove',(e)=>{let pos=e.clientXwindow.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=falsewindow.addEventListener('mousemove',(e)=>{let pos=e.clientX//若事情还在执行,则是true,不执行事件if(ticking) return//改成true,事情开始执行ticking=truewindow.requestAnimationFrame(()=>{changeWith(pos)//执行完毕,又可以变成false(又可以执行事情了)ticking=false})})</script></body></html>
这样处理之后,也就是只有本次事情处理完毕之后才能开始处理下一个事情(其实我感觉这特么是节流啊!!!!视频里面说这是防抖???)。也就是鼠标的移动事件不再根据鼠标移动来触发。而是根据raf来调度触发。保证每一秒都可以生成60帧的动画。
三,react的时间调度实现
底层的原理就是这个RAF