JavaScript DOM 编程艺术·setInterval与setTimeout的动画实现解析
先贴上moveElement()函数的大纲,为了方便观看,删了部分代码,完整版粘到文章后面。
function moveElement(elementID,final_x,final_y,interval) {
//测试JS兼容性代码
if (elem.movement) {
clearTimeout(elem.movement);
}
//计算并移动elementID位置
var repeat = "moveElement('"+elementID+"',"+final_x+","+final_y+","+interval+")";
elem.movement = setTimeout(repeat,interval);
}
第一反应
由于要实现的动画不是循环的动画(例如从上到下不停移动),
所以当时第一反应是直接用for循环,移动5个像素,然后用sleep之类的函数休眠5ms,然后再移动,直到移动到最终位置,循环结束,动画完成。
如果是这样,那么sleep函数有两种实现方式
- 阻塞式,CPU在sleep函数中停留等待一直到5ms过去
- 非阻塞式,调用sleep函数后,当前线程被挂起,JS引擎去处理其实事件(例如,此时我又点击了另一个按钮)
遗憾的是,第一种方法不可取,第二种方法不可实现。因为:
- 阻塞式中,如果CPU在SLEEP函数中停留等待,将阻塞其它事件的响应。如果整个动画时间为1S,那么在这1S内,如果用户点击了其它按钮,页面将不会对这个行为做出响应。
- JS引擎是一个运行在浏览器程序中的单线程引擎,自然没办法实现线程挂起。
事件驱动
但是很幸运(或者说是不幸)的是,JS引擎采用事件驱动,引擎在内部保存一个执行队列,JS引擎依次从队列取出事件,对事件中的JS代码进行解释执行。
例如有一按钮,
<button onclick="alert()"></button>。
当用户点击按钮后,一个onclick事件被插入到执行队列中,事件中的代码即是"alert()",JS引擎对"alert()"进行解释执行,弹出警示窗口。
此时JS引擎中的执行队列可能是:
0. 当前正在处理的事件,代码为"..."
- 正在等待处理的事件1,代码为"..."
- 正在等待处理的事件2,代码为"..."
- 正在等待处理的事件3,代码为"..."
- onclick事件,代码为"alert()"
另一条路
虽然JS引擎是单线程的,但是在浏览器中另有一个计时器线程用于计时。那么只要我们能够让计时器在5ms之后,把更新动画的事件代码插入到JS执行队列中去,那么我们就能实现非阻塞式的SLEEP功能:
- onclick事件代码:设置计时器计时5ms,代码为"move()"
- JS引擎处理其它事件
- 5ms时间到,计时器往执行队列中插入一个事件,其代码为"move()"
- JS引擎从队列中提出"move()",调用move()函数,更新动画
- 回到第1步,直到动画更新完成
setTimeout与setInterval
JS中实现对计时器进行控制的函数有两个,一个是setInterval,一个是setTimeout:
setTimeout(code, interval);
setInterval(code, interval);
两者都接收一段JS代码以及一段时间间隔为参数。区别在于:
- setTimeout函数在过了interval毫秒之后,把code代码放入JS执行队列中。
- setInterval函数则是每经过interval毫秒之后,周期性地把code代码放入JS执行队列中,直到用clearInterval()函数取消它。
实现动画
我们可以要实现一段动画,例如一张图片从左上角移到右下角,那么setTimeout和setInterval都可以实现。
setInterval实现
一般情况
- 首先,很显然用setInterval更直观更容易:
onclick函数中:用setInterval设置计时器,每过5ms将move()函数放入JS执行队列中 - move函数中:移动图片,判断图片是否到达最终位置,如是,用clearInterval取消计时器,动画完成。
这是一般情况下考虑的动画。
竞争情况
如果网页中有两个事件对图片进行移动,那么就会出现竞争的情况。JavaScript DOM编程艺术中的例子,即是这种情况。下面仿照这本书举个简单的例子:
<!-- 假设图片起始位置为(0,0) -->
<a id="a1" onmouseover="move(-150, 0)">a1</a>
<a id="a2" onmouseover="move(150, 0">a2</a>
如果我的鼠标分别从a1标签,a2标签中扫过。那么就可能会出现a1将图片往右拉,a2将图片往右拉,动画效果就会被破坏,此时JS引擎中的执行队列是这样的:
- a1:将图片往左拉5px
- ...其它事件...
- a2:将图片往右拉5px
- ...其它事件...
- a1:将图片再往左拉5px
- ...其它事件...
- ...
在极端的情况下,还有可能因此陷入死循环。
解决的方法如《JavaScript DOM编程艺术》中给出的,我们需要一个变量作为指示,来保证同一时刻只有一个事件在拉动图片。这个变量必须是任何事件函数都能够访问到的,那么理所当然我们想到的就是给图片这个结点添加一个变量movement,使其等于setInterval()的返回值。每一次有事件被触发需要移动图片时,我们就对movement变量进行检查,如果为真,证明以前有其它事件函数,姑且称为A,尝试移动图片,那么我们就用clearInterval(elem.movement)将A函数设置的计时器取消掉,中止其动画过程,自己再另外设置计时器,开始图片新的动画。
现在动画机制代码更改如下:
1. a1,a2 onmouseover: setInterval("move()", 5ms)
// 1. a1标签和a2标签的onmouseover代码:用setInterval函数设置计时器,使其每过5ms就将move()函数放入JS执行队列中去。
2. move():if elem.movement then clearInterval(elem.movement)
// 2. move函数中:判断elem.movement是否为真,如为真,则clearInterval(elem.movement),清除elem.movement绑定的以前的动画过程。至此,elem.movement一定为假。
3. update position of elem
// 3. 计算并更新elem图片新的位置。
4. if not end of movement then setInterval("move()", 5ms) ;
// 判断是否到达最终位置,到达则clearInterval(elem.movement)取消计时器,返回。如果没有到达,由于经过第二步,计时器一定不存在,所以我们需要再调用setInterval()函数设置计时器,使其每过5ms就将move函数放入JS执行队列中去。
这样,a1标签触发的动画还没有完成就触发a2标签的动画的话,那么a2标签的动画(即a2标签引发的move()函数)就会自动取消a1标签的动画,开始其自己的动画。
至时,健壮的动画功能就完成了。但是《JavaScript DOM编程艺术》中使用的是setTimeout,为什么不使用setInterval?且先看看setTimeout是如何实现动画的。
setTimeout实现
####### 一般情况 ######
由前面可以知道,setInterval的优势在于它会周期性地自动将动画函数放入JS引擎执行队列中去,但是setTimeout完全可以通过一个小技巧来实现这个功能,那就是在每次动画函数结束时,再重新设置一次计时器函数:
1. onmouseover:setTimeout("move()", 5ms)
// onmouseover函数中:用setTimeout设置5ms后将move函数放入执行队列
2. move():update position of elem
// move函数:计算并更新动画
3. if not end of movement then setTimeout("move()", 5)
// 判断动画是否结束,如未结束,用setTimeout设置计时器5ms后将move函数再放入执行队列
竞争情况
跟setInterval的情况一样,为了防止两个元素竞争移动同一个图片(或其它元素),我们可以给图片添加一个变量来指示是否有其它元素尝试移动它。同样举之前的例子:
1. a1,a2:onmouseover():setTimeout("move()", 5)
// a1,a2的onmouseover调用的函数代码中:用setTimeout设置5ms后调用move函数
2. move():if elem.movement then clearTimeout(elem.movement)
// move函数中:判断elem.movement是否为真,为真,则用clearTimeout清除之前设置的计时器。至此,elem.movement一定为假。
3. update position of elem
// 计算并更新图片位置
4. if not end of movement then elem.movement = setTimeout("move()", 5)
// 判断动画是否完成,如未完成,用setTimeout设置5ms后调用move函数。
这个就是《JavaScript DOM 编程艺术》中使用的动画机制。跟setInterval的方法非常相似,但是在不同情况下,效果还是有可能不同的。现在我们来比较一下两者,看看我们为什么要倾向于使用setTimeout而不是setInterval。
setTimeout还是setInterval
setInterval的优点:
- setInterval相比setTimeout计时更加准确。
- 在实现一般动画时,由于能自动将动画函数插入执行队列,实现起来更方便直观,不用重复设置计时器。
setInterval的缺点: - 假设我们利用setInterval设置每5ms将move函数插入执行队列中,move函数由于计算量比较大,运行时间为6ms,计时器将move函数插入执行队列后,马上又开始计时,那么move函数还未结束,计时器将会把第二个move函数插入执行队列中,导致move函数阻塞了执行队列。此时如果move函数中没有显式取消其自身计时器的话,甚至可能会出现死循环。因此如果插入执行队列的函数计算量大的话(或者周期太小),就不适合选用setInterval。
setTimeout的优点:
- setTimeout实现动画不会阻塞执行队列。因为setTimeout本身就是一次性的,在实现动画时,我们在move函数的结尾处需要再设置一次计时器。因此无论setTimeout设置了多少毫秒,假设为5ms,那么在两次move函数之间都一定会间隔开至少5ms。
setTimeout的缺点: - 在实现一般动画时,需要在函数最后再设置一次计时器。
现在看过了setInterval跟setTimeout的优缺点之后,我们再来看看为什么在竞争情况下我们使用setTimeout而不是setInterval。
经过比较我们可以看到,setInterval相比setTimeout最大的优势是自动化、方便。但是我们看setInterval实现竞争情况下的动画时,move函数伪代码应该是这样的:
1. if elem.movement then clearInterval(elem.movement)
2. update position of elem
3. if not end of movement then elem.movement = setInterval()
由于在第一步中,我们无法确定elem.movement是其它元素设置的,还是它自己在上一步动画中设置,只要elem.movement为真,我们就把计时器清除了,然后在第3步中再设置一次计时器。
也就是说在竞争情况下setInterval使用方便的优势已经丧失了,无论使用setInterval还是setTimeout我们都必须在第三步中再设置一次计时器。而setTimeout此时还具有不阻塞执行队列的优势,毕竟大多数时候动画只是呈现效果,而不是功能或内容,我们不会想要因为一个动画而把整个页面给阻塞了。
最后附上原代码:
function moveElement(elementID,final_x,final_y,interval) {
if (!document.getElementById) return false;
if (!document.getElementById(elementID)) return false;
var elem = document.getElementById(elementID);
if (elem.movement) {
clearTimeout(elem.movement);
}
if (!elem.style.left) {
elem.style.left = "0px";
}
if (!elem.style.top) {
elem.style.top = "0px";
}
var xpos = parseInt(elem.style.left);
var ypos = parseInt(elem.style.top);
if (xpos == final_x && ypos == final_y) {
return true;
}
if (xpos < final_x) {
var dist = Math.ceil((final_x - xpos)/10);
xpos = xpos + dist;
}
if (xpos > final_x) {
var dist = Math.ceil((xpos - final_x)/10);
xpos = xpos - dist;
}
if (ypos < final_y) {
var dist = Math.ceil((final_y - ypos)/10);
ypos = ypos + dist;
}
if (ypos > final_y) {
var dist = Math.ceil((ypos - final_y)/10);
ypos = ypos - dist;
}
elem.style.left = xpos + "px";
elem.style.top = ypos + "px";
var repeat = "moveElement('"+elementID+"',"+final_x+","+final_y+","+interval+")";
elem.movement = setTimeout(repeat,interval);
}
JavaScript DOM 编程艺术·setInterval与setTimeout的动画实现解析的更多相关文章
- JavaScript DOM 编程艺术
最近把JavaScript DOM 编程艺术这本书看完了,觉得这本书很好 深入浅出地展示了渐进增强.平稳退化.结构和样式分离等编程思想,我对书中重要的知识进行了梳理总结. 一.网页 二.JavaScr ...
- JavaScript DOM编程艺术(第2版)的简单总结
介绍 JavaScript DOM编程艺术(第2版)主要讲述了 JavaScript.DOM 和 HTML5 的基础知识,着重讲述了 DOM 编程,并通过几个实例演示了具有专业水准的网页开发. 下面介 ...
- 读书笔记:JavaScript DOM 编程艺术(第二版)
读完还是能学到很多的基础知识,这里记录下,方便回顾与及时查阅. 内容也有自己的一些补充. JavaScript DOM 编程艺术(第二版) 1.JavaScript简史 JavaScript由Nets ...
- JavaScript DOM编程艺术学习笔记(一)
嗯,经过了一周的时间,今天终于将<JavaScript DOM编程艺术(第2版)>这本书看完了,感觉受益匪浅,我和作者及出版社等等都不认识,无意为他们做广告,不过本书确实值得一看,也值得推 ...
- JavaScript DOM编程艺术第一章:JavaScript简史
本系列的博客是由本人在阅读<JavaScript DOM编程艺术>一书过程中做的总结.前面的偏理论部分都是书中原话,觉得有必要记录下来,方便自己翻阅,也希望能为读到本博客的人提供一些帮助, ...
- 《JavaScript dom 编程艺术》 placeholder占位符IE8兼容办法。
在<JavaScript dom 编程艺术>第11章学来的. 相对于用JavaScript替换文本框的提示语句 <!DOCTYPE html> <html lang=&q ...
- 《javascript dom编程艺术》笔记(一)——优雅降级、向后兼容、多个函数绑定onload函数
刚刚开始自学前端,如果不对请指正:欢迎各位技术大牛指点. 开始学习<javascript dom编程艺术>,整理一下学习到的知识.今天刚刚看到第六章,记下get到的几个知识点. 优雅降级 ...
- 《JavaScript DOM 编程艺术》
前几天京东买了一本书,在豆瓣上好评如潮,买下了啃一啃,书名<JavaScript DOM 编程艺术>,在好好深造一下javaScript.一边啃,一边敲.当然应该要做好笔记.一些简单的就看 ...
- JavaScript DOM编程艺术读后感(1)—— 平稳退化
最近,在读<JavaScript DOM编程艺术(第二版)>这本书,想着将自己的读后感记录下来,作为记忆吧. 其实我并不是最近才刚开始读这本书的,我读了有一段时间了.我是一名web前端开发 ...
随机推荐
- [css3]跑马灯
<div class="marquee"> <div> <p>纯CSS3生成的走马灯效果</p> <p>纯CSS3生成的 ...
- DevExpress--xtraTabbedMdiManager控件
因项目需要要实现类似jquery的Tab效果,所以要用到xtraTabbedMdiManager控件 使用xtraTabbedMdiManager一般配合navBarControl(上期已写过) 在工 ...
- Android按钮的各个样式设置
安卓开发学习之014 Button应用详解(样式.背景.按钮单击.长按.双击.多击事件) 一.Button简介 按钮也是继承自TextView 二.XML定义方法 <Button android ...
- 最简单的RASPBERRY PI wifi配置
Setting up Wifi with the Command Line SIMON MONK This tutorial works best if your router is broad ...
- Common.Logging log4net Common.Logging.Log4Net 配置
1.log4net 单独配置 log4net支持多种格式的日志输出,我这里只配置输出到本地的txt文件这种格式. <log4net> <root> <appender-r ...
- redis清空缓存
进入redis命令行 首先启动redis服务 redis-server /home/redis/redis_7901.conf redis-cli -p 7901(指定进入端口号为7901的redis ...
- freeCAD预选项编辑器
freeCAD的预选项系统在 Edit 目录 -> Preferences. freecad的功能分成不同的模块,每一模块负责一个特定的工作台工作.freecad还使用了一个概念叫晚加载,这意味 ...
- Scrum领取任务
这次主要讨论了产品的构造流程,怎么将任务分配到个人,讨论什么功能具体怎么实现,然后各自选取了任务. 在团队项目“广商百货”的SCRUM项目中我认领的任务是对登录功能的实现.现在还没正式开始,还在看书和 ...
- Hadoop的数据输入的源码解析
我们知道,任何一个工程项目,最重要的是三个部分:输入,中间处理,输出.今天我们来深入的了解一下我们熟知的Hadoop系统中,输入是如何输入的? 在hadoop中,输入数据都是通过对应的InputFor ...
- c++虚函数和内联构造函数
创建一个含有虚函数的对象时, 编译器会实现 "初始化其VPTR以指向相应的VTABLE" 这个操作 ,而实现这个操作是通过 "插入隐藏代码至构造函数中" 故此时 ...