梦想还是要有的,万一见鬼了呢。

requestAnimationFrame

早期动画循环

在js中创建动画最简单的方式是使用setInterval(),如下所示:

1
2
3
4
5
6
7
8
(function(){
function updateAnimations(){
doAnimation1()
doAnimation2()
// other animations
}
setInterval(updateAnimations,delta)
})()

编写这种动画的关键是知道delta多长合适.一方面delta越短,动画越平滑;另一方面考虑到性能的问题,delta要足够长.多数显示器的刷新频率是60Hz,因此最平滑的delta = 1000 / 60 = 17ms,但是问题来了setInterval()函数的间隔是不确定的,只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。简言之,以毫秒表示的延迟时间并不代表到时候一定会执行动画代码,而仅代表到时候会把代码添加到任务队列中。如果UI线程繁忙,比如忙于处理用户操作,那么即使把代码加入队列也不会立即执行。

另一方面,计时器的精度是不同的,IE8为15.625ms,现代浏览器的计时精度一般是4ms,更为严重的问题是浏览器会限制后台的非活动标签页,即使做了间隔时间的优化也不可能得到我们想要的结果.

为什么使用requestAnimationFrame渲染动画更好

首先思考这样一个问题:CSS动画相比与javascript动画的优势在哪?CSS变换和动画的优势在于浏览器知道动画什么时候开始,因此会计算出正确的时间间隔,在恰当的事件刷新UI,而对于javascript动画,无知晓什么时候开始.因此requestAnimationFrame()这个API就告诉浏览器某些js代码会执行动画,这样浏览器就会对其进行优化.该方法就收的参数是重绘前调用的一个函数,用于改变下一次重绘时的DOM样式.我们可以像setTimeout()一样使用:

1
2
3
4
5
6
7
8
function updateProgress(){
var div = document.getElementById('status')
div.style.width = (parseInt(div.style.width) + 5) + '%'
if(div.style.left != '100%'){
requestAnimationFrame(updateProgress)
}
}
requestAnimationFrame(updateProgress)

目前来看,mozRequestAnimationFrame()解决了浏览器不知道JavaScript动画什么时候开始、不知道最佳循环间隔时间的问题,但不知道代码到底什么时候执行的问题呢?同样的方案也可以解决这个问题。我们传递的mozRequestAnimationFrame()函数也会接收一个参数,它是一个时间码(从1970年1月1日起至今的毫秒数),表示下一次重绘的实际发生时间。注意,这一点很重要:mozRequestAnimationFrame()会根据这个时间码设定将来的某个时刻进行重绘,而根据这个时间码,你也能知道那个时刻是什么时间。然后,再优化动画效果就有了依据。要知道距离上一次重绘已经过去了多长时间,可以查询mozAnimationStartTime,其中包含上一次重绘的时间码。用传入回调函数的时间码减去这个时间码,就能计算出在屏幕上重绘下一组变化之前要经过多长时间。使用这个值的典型方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
(function(){
function draw(timestamp){
// 两次绘制的时间间隔
var drawStart = timestamp || Date.now(),
diff = drawStart - startTime

startTime = drawStart
requestAnimationFrame(draw)
}
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame,
startTime = window.mozAnimationStartTime || Date.now()
requestAnimationFrame(draw)
})()

Page Visibility API

如果页面不是处于活动状态,有些功能是可以停下来的,例如(轮询Server或者动画效果).可见性API就是针对这个而设计的.该API有3部分组成:

  • document.hidden:页面处于隐藏(后台标签页或者浏览器最小化)
  • document.visibilityState有以下4个状态:
    • 后台标签页中或浏览器最小化
    • 在前台标签页中
    • 实际的页面已经隐藏,但用户可以看到页面的预览(例如win7任务栏预览)
    • 页面在屏幕外执行渲染

visibilitychange事件,当文档从可见变为不可见或从不可见变为可见时,触发该事件,检测页面是否隐藏:

1
2
3
4
5
if (document.hidden || document.msHidden || document.webKitHidden){
//页面隐藏了
} else {
//页面未隐藏
}

以上代码在不支持该API的浏览器中会提示页面未隐藏。这是Page Visibility API有意设计的结果,目的是为了向后兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleVisibilityChange(){
var output = document.getElementById("output"),
msg;
if (document.hidden || document.msHidden || document.webkitHidden){
msg = "Page is now hidden. " + (new Date()) + "<br>";
} else {
msg = "Page is now visible. " + (new Date()) + "<br>";
}
output.innerHTML += msg;
}
//要为两个事件都指定事件处理程序
EventUtil.addHandler(document, "msvisibilitychange", handleVisibilityChange);
EventUtil.addHandler(document, "webkitvisibilitychange", handleVisibilityChange);

Geolocation API

地理定位(geolocation)是最令人兴奋,而且得到了广泛支持的一个新API。以下代码将在地图上绘制用户的位置:

1
2
3
4
5
6
7
8
9
10
navigator.geolocation.getCurrentPosition(function(position){
drawMapCenteredAt(position.coords.latitude, positions.coords.longitude);
}, function(error){
console.log("Error code: " + error.code);
console.log("Error message: " + error.message);
}, {
enableHighAccuracy: true, // 高精度,将消耗更多电量
timeout: 5000,
maximumAge: 25000
});

如果你希望跟踪用户的位置,那么可以使用另一个方法watchPosition()。这个方法接收的参数与getCurrentPosition()方法完全相同。实际上,watchPosition()与定时调用getCurrentPosition()的效果相同。在第一次调用watchPosition()方法后,会取得当前位置,执行成功回调或者错误回调。然后,watchPosition()就地等待系统发出位置已改变的信号(它不会自己轮询位置)。

调用watchPosition()会返回一个数值标识符,用于跟踪监控的操作。基于这个返回值可以取消监控操作,只要将其传递给clearWatch()方法即可(与使用setTimeout()和clearTimeout()类似)。

1
2
3
4
5
6
7
var watchId = navigator.geolocation.watchPosition(function(position){
drawMapCenteredAt(position.coords.latitude, positions.coords.longitude);
}, function(error){
console.log("Error code: " + error.code);
console.log("Error message: " + error.message);
});
clearWatch(watchId);

只能被调用一次的函数

1
2
3
4
5
6
7
8
function only_once(fn) {
var called = false;
return function() {
if (called) throw new Error("Callback was already called.");
called = true;
fn.apply(root, arguments);
};
}

以上函数返回一个只能被调用1次的函数。

1
2
3
4
function debug(){ console.log('debug') }
var fn = only_once(debug)
fn()
fn() // 第2次调用将会出错

以上代码是闭包的经典应用,出自async.js源码。

原生js模拟用户点击

1
2
3
4
5
document.body.addEventListener('click', function listener(e) {
console.log(e);
},false);

document.body.click(); // 模拟用户点击

值传递和引用传递

基本数据类型是值传递,对象是引用传递。

1
2
typeof [] // 'object'
[] == [] // false