先贴上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函数有两种实现方式

  1. 阻塞式,CPU在sleep函数中停留等待一直到5ms过去
  2. 非阻塞式,调用sleep函数后,当前线程被挂起,JS引擎去处理其实事件(例如,此时我又点击了另一个按钮)

遗憾的是,第一种方法不可取,第二种方法不可实现。因为:

  1. 阻塞式中,如果CPU在SLEEP函数中停留等待,将阻塞其它事件的响应。如果整个动画时间为1S,那么在这1S内,如果用户点击了其它按钮,页面将不会对这个行为做出响应。
  2. JS引擎是一个运行在浏览器程序中的单线程引擎,自然没办法实现线程挂起。

事件驱动

但是很幸运(或者说是不幸)的是,JS引擎采用事件驱动,引擎在内部保存一个执行队列,JS引擎依次从队列取出事件,对事件中的JS代码进行解释执行。

例如有一按钮,

<button onclick="alert()"></button>。

当用户点击按钮后,一个onclick事件被插入到执行队列中,事件中的代码即是"alert()",JS引擎对"alert()"进行解释执行,弹出警示窗口。

此时JS引擎中的执行队列可能是:

0. 当前正在处理的事件,代码为"..."

  1. 正在等待处理的事件1,代码为"..."
  2. 正在等待处理的事件2,代码为"..."
  3. 正在等待处理的事件3,代码为"..."
  4. onclick事件,代码为"alert()"

另一条路

虽然JS引擎是单线程的,但是在浏览器中另有一个计时器线程用于计时。那么只要我们能够让计时器在5ms之后,把更新动画的事件代码插入到JS执行队列中去,那么我们就能实现非阻塞式的SLEEP功能:

  1. onclick事件代码:设置计时器计时5ms,代码为"move()"
  2. JS引擎处理其它事件
  3. 5ms时间到,计时器往执行队列中插入一个事件,其代码为"move()"
  4. JS引擎从队列中提出"move()",调用move()函数,更新动画
  5. 回到第1步,直到动画更新完成

setTimeout与setInterval

JS中实现对计时器进行控制的函数有两个,一个是setInterval,一个是setTimeout:

setTimeout(code, interval);
setInterval(code, interval);

两者都接收一段JS代码以及一段时间间隔为参数。区别在于:

  1. setTimeout函数在过了interval毫秒之后,把code代码放入JS执行队列中。
  2. setInterval函数则是每经过interval毫秒之后,周期性地把code代码放入JS执行队列中,直到用clearInterval()函数取消它。

实现动画

我们可以要实现一段动画,例如一张图片从左上角移到右下角,那么setTimeout和setInterval都可以实现。


setInterval实现
一般情况
  1. 首先,很显然用setInterval更直观更容易:

    onclick函数中:用setInterval设置计时器,每过5ms将move()函数放入JS执行队列中
  2. 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引擎中的执行队列是这样的:

  1. a1:将图片往左拉5px
  2. ...其它事件...
  3. a2:将图片往右拉5px
  4. ...其它事件...
  5. a1:将图片再往左拉5px
  6. ...其它事件...
  7. ...

    在极端的情况下,还有可能因此陷入死循环。

解决的方法如《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的优点:

  1. setInterval相比setTimeout计时更加准确。
  2. 在实现一般动画时,由于能自动将动画函数插入执行队列,实现起来更方便直观,不用重复设置计时器。

    setInterval的缺点:
  3. 假设我们利用setInterval设置每5ms将move函数插入执行队列中,move函数由于计算量比较大,运行时间为6ms,计时器将move函数插入执行队列后,马上又开始计时,那么move函数还未结束,计时器将会把第二个move函数插入执行队列中,导致move函数阻塞了执行队列。此时如果move函数中没有显式取消其自身计时器的话,甚至可能会出现死循环。因此如果插入执行队列的函数计算量大的话(或者周期太小),就不适合选用setInterval。

setTimeout的优点:

  1. setTimeout实现动画不会阻塞执行队列。因为setTimeout本身就是一次性的,在实现动画时,我们在move函数的结尾处需要再设置一次计时器。因此无论setTimeout设置了多少毫秒,假设为5ms,那么在两次move函数之间都一定会间隔开至少5ms。

    setTimeout的缺点:
  2. 在实现一般动画时,需要在函数最后再设置一次计时器。

现在看过了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的动画实现解析的更多相关文章

  1. JavaScript DOM 编程艺术

    最近把JavaScript DOM 编程艺术这本书看完了,觉得这本书很好 深入浅出地展示了渐进增强.平稳退化.结构和样式分离等编程思想,我对书中重要的知识进行了梳理总结. 一.网页 二.JavaScr ...

  2. JavaScript DOM编程艺术(第2版)的简单总结

    介绍 JavaScript DOM编程艺术(第2版)主要讲述了 JavaScript.DOM 和 HTML5 的基础知识,着重讲述了 DOM 编程,并通过几个实例演示了具有专业水准的网页开发. 下面介 ...

  3. 读书笔记:JavaScript DOM 编程艺术(第二版)

    读完还是能学到很多的基础知识,这里记录下,方便回顾与及时查阅. 内容也有自己的一些补充. JavaScript DOM 编程艺术(第二版) 1.JavaScript简史 JavaScript由Nets ...

  4. JavaScript DOM编程艺术学习笔记(一)

    嗯,经过了一周的时间,今天终于将<JavaScript DOM编程艺术(第2版)>这本书看完了,感觉受益匪浅,我和作者及出版社等等都不认识,无意为他们做广告,不过本书确实值得一看,也值得推 ...

  5. JavaScript DOM编程艺术第一章:JavaScript简史

    本系列的博客是由本人在阅读<JavaScript DOM编程艺术>一书过程中做的总结.前面的偏理论部分都是书中原话,觉得有必要记录下来,方便自己翻阅,也希望能为读到本博客的人提供一些帮助, ...

  6. 《JavaScript dom 编程艺术》 placeholder占位符IE8兼容办法。

    在<JavaScript dom 编程艺术>第11章学来的. 相对于用JavaScript替换文本框的提示语句 <!DOCTYPE html> <html lang=&q ...

  7. 《javascript dom编程艺术》笔记(一)——优雅降级、向后兼容、多个函数绑定onload函数

    刚刚开始自学前端,如果不对请指正:欢迎各位技术大牛指点. 开始学习<javascript dom编程艺术>,整理一下学习到的知识.今天刚刚看到第六章,记下get到的几个知识点. 优雅降级 ...

  8. 《JavaScript DOM 编程艺术》

    前几天京东买了一本书,在豆瓣上好评如潮,买下了啃一啃,书名<JavaScript DOM 编程艺术>,在好好深造一下javaScript.一边啃,一边敲.当然应该要做好笔记.一些简单的就看 ...

  9. JavaScript DOM编程艺术读后感(1)—— 平稳退化

    最近,在读<JavaScript DOM编程艺术(第二版)>这本书,想着将自己的读后感记录下来,作为记忆吧. 其实我并不是最近才刚开始读这本书的,我读了有一段时间了.我是一名web前端开发 ...

随机推荐

  1. [css3]跑马灯

    <div class="marquee"> <div> <p>纯CSS3生成的走马灯效果</p> <p>纯CSS3生成的 ...

  2. DevExpress--xtraTabbedMdiManager控件

    因项目需要要实现类似jquery的Tab效果,所以要用到xtraTabbedMdiManager控件 使用xtraTabbedMdiManager一般配合navBarControl(上期已写过) 在工 ...

  3. Android按钮的各个样式设置

    安卓开发学习之014 Button应用详解(样式.背景.按钮单击.长按.双击.多击事件) 一.Button简介 按钮也是继承自TextView 二.XML定义方法 <Button android ...

  4. 最简单的RASPBERRY PI wifi配置

    Setting up Wifi with the Command Line  SIMON MONK   This tutorial works best if your router is broad ...

  5. Common.Logging log4net Common.Logging.Log4Net 配置

    1.log4net 单独配置 log4net支持多种格式的日志输出,我这里只配置输出到本地的txt文件这种格式. <log4net> <root> <appender-r ...

  6. redis清空缓存

    进入redis命令行 首先启动redis服务 redis-server /home/redis/redis_7901.conf redis-cli -p 7901(指定进入端口号为7901的redis ...

  7. freeCAD预选项编辑器

    freeCAD的预选项系统在 Edit 目录 -> Preferences. freecad的功能分成不同的模块,每一模块负责一个特定的工作台工作.freecad还使用了一个概念叫晚加载,这意味 ...

  8. Scrum领取任务

    这次主要讨论了产品的构造流程,怎么将任务分配到个人,讨论什么功能具体怎么实现,然后各自选取了任务. 在团队项目“广商百货”的SCRUM项目中我认领的任务是对登录功能的实现.现在还没正式开始,还在看书和 ...

  9. Hadoop的数据输入的源码解析

    我们知道,任何一个工程项目,最重要的是三个部分:输入,中间处理,输出.今天我们来深入的了解一下我们熟知的Hadoop系统中,输入是如何输入的? 在hadoop中,输入数据都是通过对应的InputFor ...

  10. c++虚函数和内联构造函数

    创建一个含有虚函数的对象时, 编译器会实现 "初始化其VPTR以指向相应的VTABLE" 这个操作 ,而实现这个操作是通过 "插入隐藏代码至构造函数中" 故此时 ...