我们可以把 JavaScript 程序的执行想象成发生在两个阶段。
第一阶段 脚本加载执行阶段
文档内容加载完成,<script> 元素指定的(内部和外部)代码运行。脚本通常按照它们在文档中出现的顺序依次加载。
任何一个脚本中的 JavaScript 代码都自上而下运行,当然还要服从 JavaScript 的条件、循环和其他控制语句。
第二阶段 事件异步处理阶段
当文档加载完毕且所有脚本都运行之后,JavaScript 执行就进入了第二阶段。
这个阶段是异步的、事件驱动的。如果脚本要在第二阶段执行,那么它在第一阶段必须要做一件事,就是至少要注册一个将异步调用的事件处理程序或其他回调函数。
客户端 JavaScript 的线程模型
JavaScript 是单线程的语言。可以保证自己写的两个事件处理程序永远不会同时运行。
在操作文档内容时,肯定不会有别的线程会同时去修改它。而且,在写 JavaScript 代码时,永远不需要关心锁、死锁或者资源争用。
Web 平台定义了一种受控的编程模型,即 Web 工作线程(Web worker)
工作线程是一个后台线程,可以执行计算密集型任务而不冻结用户界面。
工作线程中运行的代码无权访问文档内容,不会与主线程或其他工作线程共享任何状态,只能通过异步消息事件与主线程或其他工作线程通信。
客户端 JavaScript 时间线
从脚本执行阶段过渡到事件处理阶段可以进一步分成下列步骤。
-
浏览器创建 Document 对象并开始解析网页,随着对 HTML 元素及其文本内容的解析,不断向文档中添加 Element 对象和 Text 节点。此时,document.readyState 属性的值是"Loading"。
-
HTML 解析器在碰到一个没有 async、defer 或 type="module" 属性的 <script> 标签时,会把该标签添加到文档中,然后执行其中的脚本。脚本是同步执行的,而且在脚本下载和运行期间,HTML 解析器会暂停。
-
解析器在碰到一个有 async 属性集的 <script> 元素时,会开始下载该脚本的代码并继续解析文档。脚本在下载完成后会尽快执行,但解析器不会停下来等待它下载。
-
当文档解析完成后,document.readyState 属性值变成"interactive"。
-
任何有 defer 属性集的脚本都会在按照它们在文档中出现的顺序依次执行。
-
浏览器在 Document 对象上派发“DOMContentLoaded”事件。这标志着程序执行从同步脚本执行阶段过渡到了异步的事件驱动阶段。但要注意,此时仍然可能存在尚未执行的 async 脚本。
-
此时文档已经全部解析完全,但浏览器可能仍在等待其他内容(如图片)加载。当所有外部资源都加载完成,且所有 async 脚本都加载并执行完成时,document.readyState 属性值变成"complete",浏览器在 Window 对象上派发“load”事件。
-
从这一刻起,作为对用户输入事件、网络事件、定时器超时等响应,浏览器开始异步调用事件处理程序。