JavaScript 程序的执行


execution.png

客户端 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

事件和事件处理后面会详细讨论,这里只做一个快速概览。事件都有名字,比如 clickchangeload 等,用于指示发生的事件的通用类型,事件还有目标,它是一个对象,事件就是在这个对象上发生的。当我们谈论事件时,必须同时指定事件的类型和目标,比如一个单击事件(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 解析器遇到没有 asyncdefer 属性的 <script> 元素时,会把这些元素添加到文档中,然后同步执行脚本
  • 当 HTML 解析器遇到设置了 async 属性的 <script> 元素时,它开始下载脚本文本,并继续解析文档,脚本会在下载完成后执行,当文档解析完成时,document.readyState 的属性值是 interactive
  • 浏览器在 Document 对象上触发 DOMContentLoaded 事件,这标志着程序执行从同步脚本执行阶段切换到异步事件驱动阶段,但要注意,此时可能还有异步脚本没有执行完成。
  • 这时,文档已经解析完成,但是浏览器可能还要等待其他内容载入,当所有内容都已经载入,并且所有异步脚本执行完成,document.readyState 的属性值是 complete,Web 浏览器会触发 Window 对象上的 load 事件。
  • 从此刻起,就可以调用异步事件,以异步响应用户输入事件、网络事件、计时器过期等。

这是一条理想的时间线,但是所有浏览器并没有支持它的全部细节:

  • 所有浏览器都支持 load 事件;
  • DOMContentLoaded 事件都在 load 事件之前触发
  • 除 IE 之外,所有浏览器都支持 document.readyState 属性,但是属性值在不同浏览器有差别
  • 所有版本 IE 都支持 defer 属性,但其他浏览器并未实现
  • async 属性目前还不通用

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 在 HTML 中嵌入 JavaScript

>> 下一篇: 兼容性和互用性