JavaScript 时间安排与同步.docx
《JavaScript 时间安排与同步.docx》由会员分享,可在线阅读,更多相关《JavaScript 时间安排与同步.docx(11页珍藏版)》请在冰点文库上搜索。
JavaScript时间安排与同步
JavaScript时间安排与同步
JavaScript时间安排与同步
作者OlavJunkerKjær·2007年2月27日
本文翻译自TimingandSynchronizationinJavaScript
时序带来的问题是一种很棘手的JavaScriptbug。
也许在开发过程中从来没有出现过,但用户如果使用较慢的电脑或低带宽设备立即出现问题。
这些bug可能是间歇性的并很难被重现。
一个简单例子:
考虑按钮的click事件处理器中修改了此按钮下面的元素。
如果用户在后面元素被解析前就点击了按钮,则脚本会产生错误。
开发者可能永远无法发现此种错误,因为测试机器通常都是速度较快网络连接也较快的电脑,因此整个页面瞬间就被生成并显示了。
本文试图介绍各种和时间有关的JavaScript问题。
基础知识
浏览器窗口有一个线程负责运行HTML解析、事件分派和JavaScript代码执行工作。
JavaScript代码以以下两种方式之一运行:
1.在页面载入时执行的script元素中代码
2.事件处理器函数
浏览器初始化这两种方法,它们可在同一个线程中运行,但一次只能运行一个。
浏览器主要是事件驱动的(代码随着用户输入执行),但在页面加载阶段,浏览器也被解析线程驱动。
事件流
事件(event)是浏览器发出的信号,表示窗口状态发生了变化,或如不采取必要措施将发生某些变化。
事件处理器(eventhandler)是JavaScript函数,绑定在某个对象的某个事件。
若此对象发生了此事件,所有注册的事件处理器都会被执行。
所有事件处理器被顺序执行,只有一个事件处理完毕后(包括bubbling和执行默认动作)才会处理下一个事件。
默认动作
默认动作是当没有JavaScript干预时发生的动作。
如作用在链接上的click事件的默认动作是打开URL;作用在checkbox上的click事件的默认动作是选中选择框。
默认动作本身不是事件处理器,不像我们自定义的事件处理器那样可以清除或重载。
但可以使用preventDefault()(在IE中是event.returnValue)取消默认动作。
如果默认动作被取消,所有事件处理器仍被调用,但之后不会执行默认动作。
分派顺序
像load这样的事件只派发给指定对象(window或document)。
但有些事件不但会分派给目标事件,也可能会分派给祖先元素的事件处理器。
在事件被派发给目标之前,还有一个捕获(capturing)阶段,这时目标的祖先元素可以截获事件。
但事件截获不能完美的跨浏览器运行。
有些事件bubble,意思就是在分派给目标元素之后,还会分派给DOM树中所有祖先元素,直到document对象。
此特性是跨浏览器支持的。
整个向相关元素分派事件及执行默认动作的过程被称作事件分派(eventdispatch)。
非-bubbling事件分派顺序许下:
1.捕获阶段:
从上之下执行所有祖先元素的"capturing"事件处理器。
2.事件分派给目标元素,意思就是执行所有注册在此元素上的此事件的处理器(执行顺序未定义!
)
3.执行默认动作(如果没有被取消的话)
bubbling事件分派顺序如下:
1.捕获阶段:
从上之下执行所有祖先元素的"capturing"事件处理器。
2.事件分派给目标元素。
3.Bubbling阶段:
事件被分派给所有祖先元素,从目标开始沿DOM树向上。
4.执行默认动作(如果没有被取消的话)
可以使用stopPropagation()(在IE中是cancelBubble())取消事件bubbling,但仍会执行默认动作。
取消bubbling和取消默认动作是两个独立的操作。
DOM3Events规范详细介绍了事件模型的各个阶段(并含有很好的图释)。
有些情况下默认动作实际上发生在事件分派之前——但可以被取消。
例如,当checkbox被点击后,在网页中产生选中标记且在事件分派之前checked属性被更新。
但如果在分派过程中取消默认动作,在默认动作阶段更新被退回;删除选中标记,并将checked属性改回以前的值。
批量事件
有些事件成批次被创建,也就是说一个用户输入引起多个事件分派。
如当焦点从一个区域移动到另一个区域,原来的区域会产生blur-事件,新区域会产生focus事件。
从概念角度来说,这两个事件同时发生(因为是同一个用户输入产生的),但两个事件顺序被依次加入事件队列。
如果事件bubble,将完成整个事件捕获/bubbling过程,并要执行默认动作后才能分派下一个事件。
来看一个具体的例子:
在按钮上释放鼠标键,同时发生mouseup-事件和click事件。
顺序如下:
Mouseup-事件分派
1.click事件捕获阶段——执行所有捕获事件处理器
2.目标:
事件分派给目标元素。
3.mouseup事件Bubbling阶段:
事件分派至所有父元素。
4.(mouseup事件没有默认动作)
Click-事件分派
1.捕获阶段——执行所有捕获事件处理器
2.目标:
事件分派给目标元素。
3.click事件Bubbling阶段:
事件分派至所有父元素。
4.执行click事件默认动作。
每个事件分派中只能取消当前事件的默认动作。
如在mouseup事件处理器中,取消当前默认动作没有任何效果,因为mouseup事件没有默认动作。
也不能组织click事件随后立即发生,因为它们是独立的不同事件。
但默认动作可能引发另一个事件。
上面click事件如果作用在提交按钮上,则默认动作就是提交当前表单,将会分派submit事件。
所以取消click的默认动作有可能妨碍发出另一个事件。
事件队列
事件分派是用户输入的结果(鼠标或键盘),或是如页面载入结束等内部事件的结果。
但事件分派和用户输入之间是异步的。
用户输入可能发生在脚本处理器运行中。
此时会缓存用户动作,并在事件分派器可用之后为缓存的用户动作分派相应事件。
事件总是按照产生的顺序被分派,但如果事件处理器比较耗时,则事件发生和事件分派间可能存在延迟。
InternetExplorer和Mozilla在执行事件处理器时看起来完全不响应用户动作。
甚至浏览器工具栏似乎都被锁定了。
用户仍可以点击按钮,这些动作将被缓存。
比如点击按钮,将保存此动作,但是没有视觉反馈。
这让用户很疑惑,他可能认为没有检测到此动作,因此又重复点击了几次按钮,这可能导致意外结果。
或者用户会认为浏览器已经崩溃了,因为毫无反应。
Opera的反应好的多,会给用户动作视觉反馈,如另一个脚本运行时点击按钮也会有视觉反馈。
但和其他浏览器一样,仍会缓存事件并依次加入事件队列。
直到事件分派器处理事件后才会执行事件的默认动作。
这也会引起用户的疑惑,但比IE和Mozilla的假死强。
事件处理器不应该花费很多时间。
特别注意同步XMLHttpRequest请求,因为它们可能引起严重延迟。
嵌套事件
有一种特殊情况,事件不被顺序处理而是被嵌套。
如果显式使用dispatchEvent()-方法(在InternetExplorer中是fireEvent()),此事件会立刻被分派。
只有此事件完成后(并执行默认动作)才会继续执行原来事件。
DOMmutationevent(InternetExplorer不支持)也会在DOM改变时立刻同步分派事件,如调用appendChild()时。
渲染时间
通过程序对DOM或样式表做出修改,不一定能立即被渲染。
这取决于浏览器。
如通过DOM修改了元素的背景色,DOM会立刻体现此改变(DOMmutation事件会被立刻同步分派),但我们不知道浏览器合适会在屏幕上显示此变化。
在Opera中会立即显示变化,但在Mozilla和InternetExplorer中知道当前事件派发后才会显示。
Timeout
setTimeout()方法可以在一段时间后调用某个函数:
window.setTimeout(someFunction,1000);
此函数和事件处理器工作原理相似。
尽管它们响应的不是用户输入而是一段时间结束后,但处理方法和用户事件相同。
因此timeout不会精确的等待一段时间执行。
如果其他事件正在执行,则timeout脚本会被加入等待队列。
这是一个很有用的特性。
如果timeout时间为0,则函数不会被立刻执行,而是立即被加入队列。
在当前事件分派完成后(包括默认动作)将立即执行timeout函数。
如果timeout作为批量事件处理器的一部分被创建(如blur/focus,mouseup/click),timeout处理器会等待所有批量事件完成后才会被分派。
非用户事件
非用户产生事件有:
∙页面载入事件
∙Timeout事件
∙XMLHttpRequest异步获取数据后的回调
这些事件和用户事件一样被加入事件队列。
也就是说XMLHttpRequest回复处理器并不能在获取内容后立即执行,需要在事件队列中排队。
Alert
Alert对话框(及类似的confirm和prompt对话框)有一些奇怪的属性。
它们是同步的,因为启动对话框的脚本需要等待对话框被关闭。
alert()函数返回后脚本才能继续执行。
有些浏览器允许显示此对话框时接收用户输入。
也就是说一个脚本被挂起等待alert函数返回,但可能会执行另一个不同事件分派任务。
像mouseup和click这样的用户界面事件不会在显示alert对话框时被分派,因为alert是modal的并捕获了所有用户输入;但非用户产生的事件,如页面载入、timeout处理器和异步XMLHttpRequest返回处理器仍可以被分派。
页面载入
浏览器下载文档过程中逐步解析和显示HTML文档。
大多数的外部资源,如图像和插件媒体,都异步载入。
当解析器碰到img-标签或embed,iframe,object,时,会产生新线程。
解析和现实外部资源和解析显示主页面是彼此独立的。
框架和iframe中的页面也是异步载入的。
外部样式表比较特殊。
有些浏览器使用异步载入(如同图像一样),而有些浏览器使用同步载入,主要是为了避免样式表到达后不得不重新生成页面。
也就是说不要依赖此行为。
JavaScript块的执行
Script元素被同步解析。
当script元素引用外部脚本文件时,会暂停主页面解析,直到外部脚本下载、解析和执行后才继续执行。
内联JavaScript代码会在浏览器遇见结束标签时被解析和执行。
脚本块的执行
JavaScript脚本块(内联script块或外部JavaScript文件)的处理分两个阶段。
首先是解析,然后是执行。
在解析阶段会进行代码基本语法验证。
如果碰到语法错误,脚本不会被执行。
在执行阶段,所有函数之外的顶级语句都会被执行。
顶级语句可能会调用同一个代码块中定义的函数,因为函数声明在解析时已被处理。
下面的代码可以使用:
varx=getMagicNumber();
functiongetMagicNumber(){return117;}
但下面的代码无法执行,因为在运行时才会处理函数表达式。
varx=getMagicNumber();//ERROR!
getMagicNumberisundefined!
vargetMagicNumber=function(){return117;}
下面的代码也无法执行,因为每一个脚本块在碰到结束标签后立刻被解析和执行:
alert(getMessage());
functiongetMessage(){return"Hello!
";}
使用Document.write()
脚本可使用document.write()方法直接产生HTML输出。
产生的输出会被缓存直到代码块执行结束后。
然后会解析缓存的输出。
输出中可能仍包含脚本块,其解析和执行也是输出解析的一部分。
产生的HTML输出会被插入在产生此输出的脚本块之后。
DOM构建
解析器在载入页面时逐渐构建DOM。
标签解析完成后会像DOM中插入空元素。
碰到开始标签后会插入非空元素。
如当解析器开始解析元素内容时body元素就出现在DOM中。
注意元素可能和输入的HTML不同。
即使HTML中没有出现,DOM中也会创建html和head元素。
如果输入的HTML不合法,如title元素出现在body元素中,浏览器会重排DOM以使之合法。
这时DOM树的不一定按顺序创建。
推迟脚本块载入
同步载入脚本块有一个缺点:
如果页面head中需要下载和执行大量脚本代码,延迟会比较明显。
为减轻此问题,可使用script元素的defer属性。
这表明浏览器可以异步载入此脚本。
但此时无法知晓脚本合适被执行,可能在页面渲染前,也可能在页面渲染后。
Opera浏览器完全忽视defer属性。
alert("thismessagewillappearatsomeunpredictabletimeduringpageload");
被推迟的脚本不能使用document.write(),因为和解析器不是同步的。
这里需要注意的是脚本块的执行顺序严格按照出现在文档中的顺序,而不管是否含有defer属性。
也就是说如果不含defer属性的脚本出现在含有defer的脚本之后,只有解析器完全下载并执行推迟的脚本只后才能执行未推迟的脚本。
这显然削弱了defer属性的用途,因为这样的话未推迟的脚本只能放在推迟脚本之前。
因此不能依赖defer属性来安排脚本时序。
它只能让部分浏览器继续解析后面的脚本块。
渐进的页面渲染
页面渲染和DOM创建并不是同步的。
页面渲染的时序很难预料。
这取决于网络连接的顺序和页面大小;浏览器可能会等到整个页面载入结束才会渲染,也可能在网络连接较慢时分布渲染网页。
注意当页面开始显示后,用户界面就开始响应用户事件了。
这可能导致提前引用问题,如事件处理器引用还没出现的元素。
下面是一个危险的代码的例子:
O