JavaScript 程序的执行
客户端 JavaScript 没有严格的定义,我们可以说 JavaScript 程序是由 Web 页面中所包含的所有 JavaScript 代码以及引入的外部 JavaScript 文件所组成,这些代码共用同一个 Window 对象和 Document 对象,可以共享相同的全局函数和变量,如果一个脚本定义了新的全局变量和函数,那个这个变量和函数在脚本执行之后对任意 JavaScript 代码可见。
如果 Web 页面包含一个嵌入的窗体(使用<iframe>
元素引入),嵌入文档中的 JavaScript 代码和被嵌入文档里的 JavaScript 代码会有不同的全局对象,它可以当做一个单独的 JavaScript 程序。
书签里的 javascript:URL
存在于文档之外,可以当作一种用户扩展,当用户执行书签时,里面的 JavaScript 代码就可以访问全局对象和当前文档的内容,并对其进行操作。
JavaScript 代码的执行有两个阶段,第一阶段载入文档内容,并执行<script>
标签里的代码(包括内联和外部脚本),当文档载入完成,并且所有脚本执行完成后,JavaScript 执行就进入第二阶段,这个阶段是异步的,而且由事件驱动,在事件驱动阶段,Web 浏览器调用事件处理程序函数来响应异步发生的事件。事件驱动阶段里发生的第一个事件是 load
事件,指示文档已经全部载入,并可以操作。
同步、异步和延迟的脚本
JavaScript 首次添加到 Web 浏览器时,还没有任何 API 可以用来遍历和操作文档的结构和内容,当文档还在载入时,JavaScript 影响文档内容的唯一方法是使用 document.wirte()
方法生成内容并渲染到页面。
脚本的执行默认是同步阻塞的,<script>
标签可以有 defer
(延迟)和 async
(异步)属性,这可以改变脚本的执行方式(但不是所有浏览器都支持):
<script defer src="defer.js"></script>
<script async src="async.js"></script>
在 <script>
标签中使用上面两个布尔属性时,浏览器可以在下载脚本的同时继续解析和渲染文档,defer
属性使得浏览器延迟脚本的执行,直到文档的载入和解析完成,并可以操作;async
属性使得浏览器可以尽快执行脚本,而不用在下载脚本时阻塞文档解析。如果 <script>
标签同时使用了这两个属性,同时支持两者的浏览器会遵从 async
属性并忽略 defer
属性。
注意,延迟的脚本会按它们在文档里的顺序执行,而异步脚本可能会无序执行,以载入的先后顺序为准。
事件驱动的 JavaScript
事件和事件处理后面会详细讨论,这里只做一个快速概览。事件都有名字,比如 click
、change
、load
等,用于指示发生的事件的通用类型,事件还有目标,它是一个对象,事件就是在这个对象上发生的。当我们谈论事件时,必须同时指定事件的类型和目标,比如一个单击事件(click)发生在 HTMLButtonElement
对象上。
如果想要程序响应事件,需要编写一个事件处理函数(或者叫做事件监听器、回调),然后注册这个函数,这样就会在事件发生时调用它。前面提到过可以在 HTML 中添加元素属性来实现,但是不推荐这么做,最简单的方法是把 JavaScript 函数赋值给目标对象的属性,属性名都是以「on」开头,后面跟上事件名称:
window.onload = function() { ... };
document.getElementById("button").onclick = function() { ... };
对于大部分浏览器中的大部分事件来说,会把一个对象传递给事件处理函数作为参数,这个对象的属性提供了事件的详细信息(在 IE 中这些事件信息被存储在 event 对象里,而不是传递给事件处理函数)。事件处理函数的返回值有时用来指示函数是否已经充分处理了事件,以及阻止浏览器执行它默认会进行的各种操作。
有些事件的目标是文档元素,它们经常往上传递给文档树,这个过程叫做「冒泡」,例如,用户在 <button>
元素上单击鼠标,单击事件就会在按钮上触发,如果注册在按钮上的事件处理函数没有处理该事件,并且没有做冒泡停止处理,事件就会冒泡到嵌套按钮的容器元素,这样,任何注册在容器元素上的单击事件都会被调用。
如果需要为一个事件注册多个事件处理函数,或者如果想要写一个可以安全注册事件处理函数的代码模块,就算另一个模块已经为同一目标上的同一事件注册了一个处理函数,这时需要用到另一种事件处理函数注册技术。大部分可以成为事件目标的对象都有一个叫做 addEventListener()
的方法,允许注册多个监听器:
window.addEventListener("load", function() {...}, false);
这个方法的第一个参数是事件名称,目前只有 IE 9+ 实现了该方法,在 IE 8 及之前版本的浏览器中必须使用 attachEvent() 来替代它:
window.attachEvent("onload", function() { ... });
关于这两个方法我们后面还会详细讨论。
客户端 JavaScript 线程模型
JavaScript 语言核心并不包含任何线程机制,并且客户端 JavaScript 传统上也并没有定义任何线程机制。HTML 5 定义了一种作为后台线程的「WebWorker」,但是客户端 JavaScript 还是像严格的单线程一样工作。
单线程执行是为了让编程更加简单,编写代码时可以确保两个事件处理函数不会同时执行,操作文档内容时也不必担心会有其他线程试图同时修改文档,并且永远不需要在写 JavaScript 代码时担心锁、死锁和竞态条件。
单线程也有其缺点,意味着浏览器必须在脚本和事件处理函数执行停止响应用户输入,如果一个脚本执行计算密集的任务,它将会给文档载入带来延迟,从而导致用户无法在脚本完成前看到文档内容,甚至导致浏览器假死。
如果应用程序不得不执行太多的计算而导致明显的延迟,应该允许文档在执行这个计算之前全部载入,并确保告知用户计算正在进行并且浏览器没有挂起。如果可能将计算分解为离散的子任务,同时更新一个进度条向用户显示反馈则最好(比如下载、上传操作)。
HTML 5 定义了一种并发的控制方式 —— WebWorker,WebWorker 是一个用来执行计算密集任务而不冻结用户界面的后台线程,运行在 WebWorker 里的代码不能访问文档内容,不能和主线程或其他 worker 共享状态,只可以和主线程或其他 worker 通过异步事件进行通信,所以主线程不能检测并发性,而且 WebWorker 不能修改 JavaScript 的基础单线程执行模型。
客户端 JavaScript 时间线
- Web 浏览器创建 Document 对象,并且开始解析 Web 页面,解析 HTML 元素和它们的文本内容后添加 Element 对象和 Text 节点到文档中,这个阶段
document.readyState
的属性值是loading
。 - 当 HTML 解析器遇到没有
async
和defer
属性的<script>
元素时,会把这些元素添加到文档中,然后同步执行脚本 - 当 HTML 解析器遇到设置了
async
属性的<script>
元素时,它开始下载脚本文本,并继续解析文档,脚本会在下载完成后执行,当文档解析完成时,document.readyState
的属性值是interactive
。 - 浏览器在 Document 对象上触发 DOMContentLoaded 事件,这标志着程序执行从同步脚本执行阶段切换到异步事件驱动阶段,但要注意,此时可能还有异步脚本没有执行完成。
- 这时,文档已经解析完成,但是浏览器可能还要等待其他内容载入,当所有内容都已经载入,并且所有异步脚本执行完成,
document.readyState
的属性值是complete
,Web 浏览器会触发 Window 对象上的load
事件。 - 从此刻起,就可以调用异步事件,以异步响应用户输入事件、网络事件、计时器过期等。
这是一条理想的时间线,但是所有浏览器并没有支持它的全部细节:
- 所有浏览器都支持
load
事件; - DOMContentLoaded 事件都在 load 事件之前触发
- 除 IE 之外,所有浏览器都支持
document.readyState
属性,但是属性值在不同浏览器有差别 - 所有版本 IE 都支持
defer
属性,但其他浏览器并未实现 async
属性目前还不通用
No Comments