他想表达的意思是,像scroll,resize这一类的事件会非常频繁的触发,如果把太多的代码放进这一类的回调函数中,会延迟页面的滚动,甚至造成无法响应。所以应该把这一类代码分离出来,放在一个timer中,有间隔的去检查是否滚动,再做适当的处理。比如如下代码:
var didScroll = false; $(window).scroll(function() { didScroll = true; }); setInterval(function() { if ( didScroll ) { didScroll = false; // Check your page position and then // Load in more results } }, 250)
这样的作法类似于Nicholas将需要长时间运算的循环分解为“片”来进行运算:
// 具体可以参考他写的《javascript高级程序设计》 // 也可以参考他的这篇博客: http://www.gxlcms.com/ function chunk(array, process, context){ var items = array.concat(); //clone the array setTimeout(function(){ var item = items.shift(); process.call(context, item); if (items.length > 0){ setTimeout(arguments.callee, 100); } }, 100); }
原理其实是一样的,为了优化性能、为了防止浏览器假死,将需要长时间运行的代码分解为小段执行,能够使浏览器有时间响应其他的请求。
回到rAF上来,其实rAF也可以完成相同的功能。比如最初的滚动代码是这样:
function onScroll() { update(); } function update() { // assume domElements has been declared for(var i = 0; i < domElements.length; i++) { // read offset of DOM elements // to determine visibility - a reflow // then apply some CSS classes // to the visible items - a repaint } } window.addEventListener('scroll', onScroll, false);
这是很典型的反例:每一次滚动都需要遍历所有元素,而且每一次遍历都会引起reflow和repaint。接下来我们要做的事情就是把这些费时的代码从update中解耦出来。
首先我们仍然需要给scroll事件添加回调函数,用于记录滚动的情况,以方便其他函数的查询:
var latestKnownScrollY = 0; function onScroll() { latestKnownScrollY = window.scrollY; }
接下来把分离出来的repaint或者reflow操作全部放入一个update函数中,并且使用rAF进行调用:
function update() { requestAnimationFrame(update); var currentScrollY = latestKnownScrollY; // read offset of DOM elements // and compare to the currentScrollY value // then apply some CSS classes // to the visible items } // kick off requestAnimationFrame(update);
其实解耦的目的已经达到了,但还需要做一些优化,比如不能让update无限执行下去,需要设标志位来控制它的执行:
var latestKnownScrollY = 0, ticking = false; function onScroll() { latestKnownScrollY = window.scrollY; requestTick(); } function requestTick() { if(!ticking) { requestAnimationFrame(update); } ticking = true; }
并且我们始终只需要一个rAF实例的存在,也不允许无限次的update下去,于是我们还需要一个出口:
function update() { // reset the tick so we can // capture the next onScroll ticking = false; var currentScrollY = latestKnownScrollY; // read offset of DOM elements // and compare to the currentScrollY value // then apply some CSS classes // to the visible items } // kick off - no longer needed! Woo. // update();
Kyle Simpson说:
Rule of thumb: don’t do in JS what you can do in CSS.
如以上所说,即使使用rAF,还是会有诸多的不便。我们还有一个选择是使用css动画:虽然浏览器中UI线程与js线程是互斥,但这一点对css动画不成立。
在这里不聊css动画的用法。css动画运用的是什么原理来提升浏览器性能的。
首先我们看看淘宝首页的焦点图:
我想提出一个问题,为什么明明可以使用translate 2d去实现的动画,它要用3d去实现呢?
我不是淘宝的员工,但我的第一猜测这么做的原因是为了使用translate3d hack。简单来说如果你给一个元素添加上了-webkit-transform: translateZ(0);或者-webkit-transform: translate3d(0,0,0);属性,那么你就等于告诉了浏览器用GPU来渲染该层,与一般的CPU渲染相比,提升了速度和性能。(我很确定这么做会在Chrome中启用了硬件加速,但在其他平台不做保证。就我得到的资料而言,在大多数浏览器比如Firefox、Safari也是适用的)。
但这样的说法其实并不准确,至少在现在的Chrome版本中这算不上一个hack。因为默认渲染所有的网页时都会经过GPU。那么这么做还有必要吗?有。在理解原理之前,你必须先了解一个层(Layer)的概念。
html在浏览器中会被转化为DOM树,DOM树的每一个节点都会转化为RenderObject, 多个RenderObject可能又会对应一个或多个RenderLayer。浏览器渲染的流程如下:
获取 DOM 并将其分割为多个层(RenderLayer)
将每个层栅格化,并独立的绘制进位图中
将这些位图作为纹理上传至 GPU
复合多个层来生成最终的屏幕图像(终极layer)。
这和游戏中的3D渲染类似,虽然我们看到的是一个立体的人物,但这个人物的皮肤是由不同的图片“贴”和“拼”上去的。网页比此还多了一个步骤,虽然最终的网页是由多个位图层合成的,但我们看到的只是一个复印版,最终只有一个层。当然有的层是无法拼合的,比如flash。以爱奇艺的一个播放页(http://www.gxlcms.com/)为例,我们可以利用Chrome的Layer面板(默认不启用,需要手动开启)查看页面上所有的层:
我们可以看到页面上由如下层组成:
OK,那么问题来了。
假设我现在想改变一个容器的样式(可以看做动画的一个步骤),并且是一种最糟糕的情况,改变它的长和宽——为什么说改变长和宽是最糟糕的情况呢。通常改变一个物体的样式需要以下四个步骤:
任何属性的改变都导致浏览器重新计算容器的样式,比如你改变的是容器的尺寸或者位置(reflow),那么首先影响的就是容器的尺寸和位置(也影响了与它相关的父节点自己点相邻节点的位置等),接下来浏览器还需要对容器重新绘制(repaint);但如果你改变的只是容器的背景颜色等无关容器尺寸的属性,那么便省去了第一步计算位置的时间。也就是说如果改变属性在瀑布图中开始的越早(越往上),那么影响就越大,效率就越低。reflow和repaint会导致所有受影响节点所在layer的位图重绘,反复执行上面的过程,导致效率降低。
为了把代价降到最低,当然最好只留下compositing layer这一个步骤即可。假设当我们改变一个容器的样式时,影响的只是它自己,并且还无需重绘,直接通过在GPU中改变纹理的属性来改变样式,岂不是更好?这当然是可以实现的,前提是你有自己的layer
这也是上面硬件加速hack的原理,也是css动画的原理——给元素创建自己layer,而非与页面上大部分的元素共用layer。
什么样的元素才能创建自己layer呢?在Chrome中至少要符合以下条件之一:
Layer has 3D or perspective transform CSS properties(有3D元素的属性)
Layer is used by <video> element using accelerated video decoding(video标签并使用加速视频解码)
Layer is used by a <canvas> element with a 3D context or accelerated 2D context(canvas元素并启用3D)
Layer is used for a composited plugin(插件,比如flash)
Layer uses a CSS animation for its opacity or uses an animated webkit transform(CSS动画)
Layer uses accelerated CSS filters(CSS滤镜)
Layer with a composited descendant has information that needs to be in the composited layer tree, such as a clip or reflection(有一个后代元素是独立的layer)
Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer is rendered on top of a composited layer)(元素的相邻元素是独立layer)
很明显刚刚我们看到的播放页中的flash和开启了translate3d样式的焦点图符合上面的条件。
同时你也可以勾选Chrome开发工具中的rendering选显卡下的Show composited layer borders 选项。页面上的layer便会加以边框区别开来。为了验证我们的想法,看下面这样一段代码:
<html> <head> <style type="text/css"> p { -webkit-animation-duration: 5s; -webkit-animation-name: slide; -webkit-animation-iteration-count: infinite; -webkit-animation-direction: alternate; width: 200px; height: 200px; margin: 100px; background-color: skyblue; } @-webkit-keyframes slide { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(120deg); } } </style> </head> <body> <p id="foo">I am a strange root.</p> </body> </html>
运行时的timeline截图如下:
可见元素有自己的layer,并且在动画的过程中没有触发reflow和repaint。
最后再看看淘宝首页,不仅仅只有焦点图才拥有了独立的layer:
但太多的layer也未必是一件好事情,有兴趣的同学可以看一看这篇文章:Jank Busting Apple’s Home Page。看一看在苹果首页太多layer时出现的问题。