文章目录
定时器
setInterval
//test.html
<body>0
<script src="./test.js"></script>
</body>
//test.js
var count = Number(document.body.textContent);
var timer = setInterval(function(){
document.body.textContent = ++count;
},1000);
setInterval存在的问题
setInterval(function(){
//dosomething ...
},200)
使用setInterval
来重复定时器任务可能会出现两个问题:
- 丢帧,某个时间间隔可能会被丢掉
由于405ms
时的定时器任务仍然在任务队列中,所以605ms
时的定时器任务被丢弃 - 定时器任务执行的时间间隔可能会小于指定的时间间隔
205ms
时的定时器任务在主线程执行完毕,主线空闲下来,405ms
时的定时器任务立即从任务队列进入主线程开始执行,前后的时间间隔小于200ms
。
setTimeout模拟setInterval
function fn(){
document.body.textContent = ++count;
setTimeout(fn,1000);
}
//fn()
setTimeout(fn,1000);
setTimeout
能够保证只有前一个setTimeout任务
执行完毕,才会创建一个新的setTimeout任务
,这样不会丢帧,也能保证setTimeout任务
执行的时间间隔至少为指定的时间间隔。
呃,还有个问题,怎么把定时器停下来?
var count = 0;
function fn(){
console.log(count++);
if(count === 10){
fn = null;
}
setTimeout(fn,1000);
}
setTimeout(fn,1000);
使用定时器来实现动画
//test.html
<html>
<head>
<link rel="stylesheet" href="./test.css">
</head>
<body>
<div class="box"></div>
<div class="start">开始</div>
<div class="reset">重置</div>
<script src="./test.js"></script>
</body>
</html>
//test.css
.box{
background:#fb3;
width:100px;height:100px;
position:absolute;
}
.start,.reset{
position:absolute;
top:120px;
background-color:orange;
color:white;
font-size:0.75em;
padding:0.5em;
cursor:pointer;
}
.reset{
left:5em;
}
.start:hover,.reset:hover{
background-color:olive;
}
//test.js
var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));
var original = left;
const interval = 16;
// var last;
function move(){
// var current = Date.now();
// if(last == undefined) last = current;
left += 10;
// console.log("时间差:",current-last," left:",left);
// last = current;
box.style.left = left + "px";
if(left < 200){
setTimeout(move,interval);
}
}
var start = document.querySelector(".start");
start.onclick = function(){
setTimeout(move,interval);
}
function back(){
left -= 10;
box.style.left = left + "px";
if(left > original){
setTimeout(back,interval);
}
}
var reset = document.querySelector(".reset");
reset.onclick = function(){
left = Number(window.getComputedStyle(box).left.slice(0,-2));
setTimeout(back,interval);
}
每秒渲染60帧
,即 约16ms
渲染1帧
能够获得比较流畅的动画效果,于是将时间间隔指定为16.666ms
,以期望定时器 每隔16.666ms
改变一次 绝对定位元素box的left值,从而实现动画。代码中指定延时时间为16ms
存在的问题
如果定时器能保证 每隔 16ms
就一定会调用一次回调函数,那么动画效果确实差强人意。
可惜,不是!
定时器作为作为浏览器的异步线程之一,它会兢兢业业地计着时,但每过16ms
,回调函数会不会被执行,它做不了主,因为所有的同步任务都是在js引擎线程上执行。
如果同步任务太多,js引擎线程忙不过来,那么定时器回调函数就会一直在任务队列里待着,待着待着,时间慢慢过去了,box不偏移了,动画也就卡在那儿了。
也就是说,定时器无法保证回调函数按照指定的时间间隔被调用从而导致动画卡顿,这就是使用定时器实现动画的缺点。
好了,既然出现问题了,就得想法子解决它。
解决方法
认识requestAnimationFrame
window.requestAnimationFrame(callback)
,字面意思是“请求动画帧”。requestAnimationFrame(callback)
会请求浏览器在 下次刷新前 调用回调函数。浏览器的刷新频率是60Hz
,即每秒刷新60次,所以requestAnimationFrame
里的回调函数 每秒会被调用60次。
看下面一个例子,借requestAnimationFrame
计算浏览器的刷新频率。
var startTime;
var count = 0;
function render(timeStamp){
if(!startTime) startTime = timeStamp;
count++;
if(count%100 === 0){
var time = (timeStamp - startTime)/1000;
var f = Math.round(count / time);
console.log(f);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
requestAnimationFrame(callback)
的回调函数callback
接受一个参数,这个参数timeStamp
是一个时间戳,表示开始执行回调函数的时刻,和window.performance.now()
是一样的。
举个例子理解一下。
function doSomething(){
console.log("hello world");
}
let t0;
let t1;
console.log(t0 = window.performance.now());
doSomething();
console.log(t1 = window.performance.now());
console.log("doSomething 执行了"+(t1-t0)+"毫秒");
时间戳相减,我们就知道执行某个函数消耗了多长时间。
使用requestAnimationFrame实现动画
前面我们提过,每秒渲染60帧
能够获得较流畅的动画效果,而requestAnimationFrame
刚好能够帮我们协调到这么一个节奏。
var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));
var start = document.querySelector(".start");
start.onclick = function(){
// var last;
function move(timeStamp){
// if(last == undefined) last = timeStamp;
left += 10;
box.style.left = left + "px";
// console.log("时间差:",(timeStamp-last).toFixed(3)," left:",left);
// last = timeStamp;
if(left < 200){
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);
}
相较于setTimeout
,使用requestAnimationFrame
来实现动画,效果更理想。
另外,有requestAnimationFrame
,就有cancelAnimationFrame
。
好了,问题解决了。继续深入下,了解下requestAnimationFrame
的实现。
requestAnimationFrame的实现
var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));
var start = document.querySelector(".start");
start.onclick = function(){
var startTimeStamp = Date.now();
var lastTimeStamp = startTimeStamp;
function requestAnimationFrame(callback){
var currentTimeStamp = Date.now();
var diff = currentTimeStamp - lastTimeStamp;
var delay = diff>16 ? 0 : (16-diff);
setTimeout(() => {
callback(lastTimeStamp = currentTimeStamp+delay);
},delay);
}
// var last;
function move(timeStamp){
// if(last == undefined) last = timeStamp;
left += 10;
// console.log("时间差:",timeStamp-last," left:",left);
// last = timeStamp;
box.style.left = left + "px";
if(left < 200){
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);
}
var start = document.querySelector(".start");
start.onclick = function(){
var startTime = Date.now();
var lastedTime = startTime;
function requestAnimationFrame(fn){
var currentTime = Date.now();
var diff = currentTime - lastedTime;
var delay = diff>16?0:(16-diff);
setTimeout(function(){
fn(currentTime+delay);
},delay)
lastedTime = currentTime;
}
function move(timeStamp){
var now = Date.now();
var left = 8+ (now-startTime)/16;
if(left<200){
box.style.left = left + "px";
requestAnimationFrame(move);
}
}
requestAnimationFrame(move);
}
这里涉及到两个时间戳。
currentTimeStamp
本次动画开始执行的时刻lastTimeStamp
上次动画开始执行的时刻
diff = currentTimeStamp - lastTimeStamp
,我们会判断这两次动画的时间间隔diff
是否大于16ms
。
如果大于,那么setTimeout
中延时将设置为0
,从而保证只要js引擎线程空闲,就立即执行定时器的回调函数来更新动画
如果小于,setTimeout
中延时将设置为(16-diff)
,也就是说,即使js引擎线程空闲,也要等待(16-diff)
这么长时间,才会执行回调函数来更新动画。
相较于setTimeout(callback,interval)
,期望以固定的时间间隔来调用回调然并卵,它实现不了,requestAnimationFrame(callback)
会更灵活地选择时机来调用回调:如果上次太慢,耽搁了进度,这次就不等了,立即执行;如果上次给力,时间充裕,这次就不着急,可以等等。
定时器助力函数防抖节流
不论是bind的实现,还是函数节流防抖,采用了 函数柯里化。
瞧,它们的写法是相同的。
- bind的实现
const slice = Array.prototype.slice;
//bind的实现
function bind(fn,context){
var args = slice.call(arguments,2);
return function(){
var finalArgs = args.concat(slice.call(arguments));
fn.apply(context,finalArgs);
}
}
- 函数防抖
const slice = Array.prototype.slice;
//函数防抖
function debounce(fn,context,delay=100){
var timer = null;
var args = slice.call(arguments,3);
return function(){
if(timer!==null) clearTimeout(timer);
var finalArgs = args.concat(slice.call(arguments));
timer = setTimeout(() => {
fn.apply(context,finalArgs);
},delay);
}
}
我们来看下它的应用吧,最典型的就是resize事件
了。
改变浏览器窗口大小、最大化或最小化浏览器窗口都会触发resize事件
。如果resize事件
的事件程序中包含了大量的DOM操作,它们将占用较多的内存,消耗较多的CPU计算能力,由此可能导致浏览器挂起甚至崩溃。这时候,函数防抖 就派上用场了。
const handler = function(){
console.log("resizing");
}
// document.body.onresize = handler; //不防抖
document.body.onresize = debounce(handler); //防抖
如果上一次的定时器任务执行完毕,那么clearTimeout(timer)
没啥意义;
如果上一次的定时器任务没有执行完,那么clearTimeout(timer)
将取消掉上一次的定时器任务,并setTimeout()
来创建新的定时器任务。这样一来,即使100ms内连续触发了20次resize事件
,其事件处理程序也只会执行一次。
- 函数节流
const slice = Array.prototype.slice;
//函数节流
function throttle(fn,context,delay=100){
var timer = null;
var args = slice.call(arguments,3);
return function(){
if(timer!=null) return;
var finalArgs = args.concat(slice.call(arguments));
timer = setTimeout(() => {
fn.apply(context,finalArgs);
timer = null;
},delay);
}
}
函数节流的典型应用就是scroll事件
了。连续触发scroll事件
,单位时间内触发一次每隔一段时间会触发一次
body{
height:1000px;
}
var count = 0;
const handler = function(){
count++;
var t2 = window.performance.now();
var diff = (t2-t1)/1000;
console.log("scroll",Math.floor(count/diff));
}
var t1 = window.performance.now();
//document.body.onscroll = handler; //不节流
document.body.onscroll = throttle(handler);//节流