问题1:60Hz和60fps有什么关系?
没有任何关系。fps代表GPU渲染画面的频率,Hz代表显示器刷新屏幕的频率。一幅静态图片,你可以说这副图片的fps是0帧/秒,但绝对不能说此时屏幕的刷新率是0Hz,也就是说刷新率不随图像内容的变化而变化。游戏也好浏览器也好,我们谈到掉帧,是指GPU渲染画面频率降低。比如跌落到30fps甚至20fps,但因为视觉暂留原理,我们看到的画面仍然是运动和连贯的。
实际情况会比以上想象的复杂的多。即使你能给出一个固定的延时,解决60Hz屏幕下丢帧问题,那么其他刷新频率的显示器应该怎么办,要知道不同设备、甚至相同设备在不同电池状态下的屏幕刷新率都不尽相同。
以上同时还忽略了屏幕刷新画面的时间成本。问题产生于GPU渲染画面的频率和屏幕刷新频率的不一致:如果GPU渲染出一帧画面的时间比显示器刷新一张画面的时间要短(更快),那么当显示器还没有刷新完一张图片时,GPU渲染出的另一张图片已经送达并覆盖了前一张,导致屏幕上画面的撕裂,也就是是上半部分是前一张图片,下半部分是后一张图片:
在Javascript高性能动画与页面渲染这个文章的图中,例如在14ms的时候,那么只有所有的帧数加起来时间超过16.7ms,那么肯定会丢帧,而每一帧延时这时候是2.7ms,因此16.7/2.7=6也就是在第六帧的时候肯定会丢帧!!如果更新频率为16ms,那么每一帧延迟为0.7ms,那么在16.7/0.7=23.85也就是在24帧的时候肯定会掉帧!!!而且如果动画setInterval的时间间隔越小,那么掉帧的数量会越多!
PC游戏中解决这个问题的方法是开启垂直同步(v-sync),也就是让GPU妥协,GPU渲染图片必须在屏幕两次刷新之间,且必须等待屏幕发出的垂直同步信号。但这样同样也是要付出代价的:降低了GPU的输出频率,也就降低了画面的帧数。以至于你在玩需要高帧数运行的游戏时(比如竞速、第一人称射击)感觉到“顿卡”,因为掉帧。(丢帧导致的问题就是卡顿,因为在运行其他的任务去了)
但如果你的回调函数耗时真的很严重,rAF还是可以为你做一些什么的。比如当它发现无法维持60fps的频率时,它会把频率降低到30fps,至少能够保持帧数的稳定,保持动画的连贯。
问题2:UI引擎和JS引擎是如何互斥的?
function jank(second) { var start = +new Date(); while (start + second * 1000 > (+new Date())) {} } div.style.backgroundColor = "red"; // some long run task //UI引擎和JS引擎互斥,先调用UI引擎,然后JS引擎,最后又是UI引擎 jank(5); div.style.backgroundColor = "blue";
无论在任何的浏览器中运行上面的代码,你都不会看到div变为红色,页面通常会在假死5秒,然后容器变为蓝色。这是因为浏览器的始终只有一个线程在运行(可以这么理解,因为js引擎与UI引擎互斥,浏览器不会马上更新UI,而是会采用队列化修改批量更新的方式来完成的,除非是特殊的属性如innerWidth等,否则肯定会先执行后面的JS代码的)。虽然你告诉浏览器此时div背景颜色应该为红色,但是它此时还在执行脚本,无法调用UI线程。
var div = document.getElementById("foo"); var currentWidth = div.innerWidth; div.style.backgroundColor = "blue"; //这里的UI也不会马上更新,而是会等到后面的JS运行结束后才会执行的 // do some "long running" task, like sorting data
我们可以用下面的代码进行优化:
requestAnimationFrame(function(){ var el = document.getElementById("foo"); var currentWidth = el.innerWidth; el.style.backgroundColor = "blue"; }); // do some "long running" task, like sorting data
更新背景颜色的代码过于提前,根据前一个例子,我们知道,即使在这里告知了浏览器我需要更新背景颜色,浏览器至少也要等到js运行完毕才能调用UI线程;(延迟这部分的代码)假设后面部分的long
runing代码会启动一些异步代码,比如setTimeout或者Ajax请求又或者web-worker,那应该尽早为妙(让后续代码马上执行)。
问题3:window.onscroll的弊端有哪些
像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)
我们也可以采用requestAnimationFrame的方式完成处理:
var latestKnownScrollY = 0; function onScroll() { latestKnownScrollY = window.scrollY; } 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);
这种方式不管我们的latestKnownScrollY是否发生变化,也就是是否发生滚动,那么我们都会不停的执行update方法,这显然是不需要的,例如用户在一个页面一直停留了半个小时而没有发生滚动,这时候的计算是没有必要的。
var latestKnownScrollY = 0, ticking = false; //每次滚动的时候都会计算当前滚动的距离,同时也会采用requestAnimationFrame来更新下一帧的内容! function onScroll() { latestKnownScrollY = window.scrollY; requestTick(); } function requestTick() { if(!ticking) { requestAnimationFrame(update); //如果<下一帧执行完成后>这时候又可以继续监听滚动事件了,而不是像上面的例子每一次不管滚动 //因此,即使你已经滚动的某个元素,但是因为这时候update没有执行,也就是上一帧还没有执行,因此不会继续往里面添加回调函数的,但是滚动的最新距离是可以获取到的 } ticking = true; } 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 } window.addEventListener('scroll', onScroll, false);
注意:在这里不断的滚动是不会插入多个upate方法的,因为ticking为true了,所以只有等到下一帧执行了这个update方法才能继续插入upate,但是我们要注意,这时候页面的垂直滚动距离是不断变化的!
问题4:我们如何把一个函数推迟到下下帧来执行
(function(h5){ if (!h5) throw new Error("animationFrame.h5ive: core.h5ive required."); var rAF = (window.requestAnimationFrame || window.msRequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.oRequestAnimationFrame), cAF = (window.cancelAnimationFrame || window.msCancelAnimationFrame || window.msCancelRequestAnimationFrame || window.mozCancelAnimationFrame || window.mozCancelRequestAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.oCancelAnimationFrame || window.oCancelRequestAnimationFrame), publicAPI, q_ids = {} ; //产生一个独一无二的ID值 function qID(){ var id; do { id = Math.floor(Math.random() * 1E9); } while (id in q_ids); return id; } //插入下一帧,返回的是这个独一无二的数字 function queue(cb) { var qid = qID(); //通过这个独一无二的ID来向回调函数中放置回调函数。q_ids[111111113334]=12,q_ids[456789098]=24。调用返回的一个唯一的整数值(浏览器来返回的),通过这个整数值就能够取消这一次的调用!!! q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); //这个元素的回调函数中的参数是除了回调函数以外给queue传入的参数 }); return qid; } //插入下下一帧,queue的作用是把函数放在下一帧中去执行,不过这个qid是全局唯一的! function queueAfter(cb) { var qid; qid = queue(function(){ // do our own rAF call here because we want to re-use the same `qid` for both frames q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); }); }); return qid; } //取消某一个回调 function cancel(qID) { if (qID in q_ids) { cAF(q_ids[qID]); delete q_ids[qID]; } return publicAPI; } function unsupported() { throw new Error("'requestAnimationFrame' not supported."); } //如果支持RAF那么就走这里的逻辑 if (rAF && cAF) { publicAPI = { queue: queue, queueAfter: queueAfter, cancel: cancel }; } else { publicAPI = { queue: unsupported, queueAfter: unsupported, cancel: unsupported }; } h5.animationFrame = publicAPI; })(this.h5);
因为每一个rAF函数都会返回一个独一无二的整数,而且这个整数是由浏览器决定的。通过这个整数我们可以取消某一个调用。
function queue(cb) { var qid = qID(); //通过这个独一无二的ID来向回调函数中放置回调函数。q_ids[111111113334]=12,q_ids[456789098]=24。调用返回的一个唯一的整数值(浏览器来返回的),通过这个整数值就能够取消这一次的调用!!! q_ids[qid] = rAF(function(){ delete q_ids[qid]; cb.apply(publicAPI,arguments); //这个元素的回调函数中的参数是除了回调函数以外给queue传入的参数 }); return qid; }
这个函数是把函数推迟到下一帧执行,而且每次都会产生一个独一无二的qid值作为数组的键,而值是rAF函数返回的一个唯一的值。当下一帧这个函数执行后那么我们就会从这个数组中清除相应的键和值,但是这里采用的delete删除,因此会留下undefined!
参考文献: