生命周期概览

image.png
从用户的角度来说,浏览器构建了发送至服务器的请求,该服务器处理了请求并形成了一个通常由HTML、CSS和JavaScript代码所组成的响应
当浏览器接受了响应时,我们的客户端应用开始了它的生命周期。由于客户端Web应用是图形用户界面(GUI)应用,其生命周期与其他的GUI应用相似(例如标准的桌面应用或移动应用),其执行步骤如下所示:

  • 页面构建——创建用户界面
  • 事件处理——进入循环从而等待事件的发生,发生后调用事件处理器

应用的生命周期随着用户关掉或离开页面而结束。

页面构建阶段

当Web应用能被展示或交互之前,其页面必须根据服务器获取的响应(通常是HTML、CSS和JavaScript代码)来构建。页面构建阶段的目标是建立Web应用的UI,其主要包括两个步骤:

  1. 解析HTML代码并构建文档对象模型(DOM)
  2. 执行JavaScript代码

步骤1会在浏览器处理HTML节点的过程中执行,步骤2会在HTML解析到一种特殊节点——脚本节点(包含或引用JavaScript代码的节点)时执行。页面构建阶段中,这两个步骤会交替执行多次。
image.png

HTML解析和DOM构建

页面构建阶段始于浏览器接收HTML代码时,该阶段为浏览器构建页面UI的基础。通过解析收到的HTML代码,构建一个个HTML元素,构建DOM。在这种对HTML结构化表示的形式中,每个HTML元素都被当做一个节点。如图所示,知道遇到第一个脚本元素,页面都在构建DOM。

  • 除了第一个节点——html根节点以外,所有节点都只有一个父节点。
  • 一个节点可以有任意数量的子节点。
  • 同一元素的孩子节点被称作兄弟节点。

image.png
我们可以把HTML代码看作浏览器页面UI构建初始DOM的蓝图,为了正确构建每个DOM,浏览器还会修复它在蓝图中发现的问题。例如页面中的head元素中错误地包含了一个paragraph元素。
head元素的一般用途是展示页面的总体信息,例如页面标题、字符编码和外部样式脚本,而不能用于定义页面内容。
故而这里出现了错误,浏览器会静默修复错误,将段落元素放入了理应放置页面内容的body元素中,构造正确的DOM。
image.png

HTML5规范:https://html.spec.whatwg.org/https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5 DOM3规范:https://dom.spec.whatwg.org/https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model

在页面构建阶段,浏览器会遇到特殊类型的HTML元素——脚本元素,该元素用于包括JavaScript代码。每当解析到脚本元素时,浏览器就会停止从HTML构建DOM,并开始执行JavaScript代码。

执行JavaScript代码

所有包含脚本元素中的JavaScript代码由浏览器的JavaScript引擎执行,例如Firefox的Spidermonkey引擎,Chrome和Opera的V8引擎和Edge的Chakra引擎。由于代码的主要目的是提供动态页面,故而浏览器通过全局对象提供了一个API是JavaScript引擎可以与之交互并改变页面内容。

JavaScript的全局对象

浏览器暴露给JavaScript引擎的主要全局对象是window对象,它代表了包含着一个页面的窗口。window对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器API的访问途径。
全局window对象最重要的属性是document,它代表了当前页面的DOM。通过使用这个对象,JavaScript代码就能在任何程度上改变DOM,包括修改或移除现存的节点,以及创建和插入新的节点。

JavaScript代码的不同类型

两种不同类型的JavaScript代码:全局代码和函数代码

  1. function addMessage(element,message){
  2. // 函数代码
  3. const messageElement = document.createElement("li");
  4. messageElement.textContent = message;
  5. element.appendChild(messageElement);
  6. }
  7. // 全局代码
  8. const first = document.getElementById("first");
  9. addMessage(first,"Page loading");

这两类代码的主要不同是它们的位置:包含在函数内的代码叫做函数代码,而在所有函数以外的代码叫全局代码。
这两种代码在执行中也有不同:全局代码有JavaScript引擎以一种直接的方式自动执行,每当遇到这样的代码就一行一行地执行。而执行函数代码,则需要被其他代码调用:既可以是全局代码,也可以是其他函数,还可以由浏览器调用。
image.png

在页面构建阶段执行JavaScript代码

当浏览器在页面构建阶段遇到了脚本节点,它会停止HTML到DOM的构建,转而开始执行JavaScript代码,也就是执行包含在脚本元素中的全局JavaScript代码。

一般来说,JavaScript代码能够在任何程度上修改DOM结构:它能创建新的节点或移除现有DOM节点。但是它依然不能做某些事,例如选择和修改还没被创建的节点。这就是为什么要把script元素放在页面底部的原因。如此一来,我们就不必担心是否某个HTML元素已经加载为DOM。

一旦JavaScript引擎执行到脚本元素中JavaScript代码的最后一行,浏览器就退出了JavaScript执行模式,并继将余下的HTML构建为DOM节点。
在这期间,如果浏览器再次遇到脚本元素,那么从HTML到DOM的构建再次暂停,JavaScript运行环境开始执行余下的JavaScript代码。
需要注意的是:JavaScript应用在此时依然会保持全局状态,所有在某个JavaScript代码执行期间用户创建的全局变量都能正常地被其他脚本元素中的JavaScript代码所访问到。其原因是在于全局window对象会存在于整个页面的生存期之间,在它上面存储着所有JavaScript变量。
只要还有没处理完的HTML元素和没执行完的JavaScript代码,下面两个步骤就会一直交替执行:将HTML构建为DOM和执行JavaScript代码。

实例

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Web Application Lifecycle</title>
  7. <style>
  8. #first{
  9. color: green;
  10. }
  11. #second{
  12. color: red;
  13. }
  14. </style>
  15. </head>
  16. <body>
  17. <ul id="first"></ul>
  18. <script>
  19. function addMessage(element,message){
  20. const messageElement = document.createElement("li");
  21. messageElement.textContent = message;
  22. element.appendChild(messageElement);
  23. }
  24. const first = document.getElementById("first");
  25. addMessage(first,"Page loading");
  26. </script>
  27. <ul id="second"></ul>
  28. </body>
  29. </html>

执行过程:
首先定义了一个addMessage函数:

function addMessage(element,message){
  const messageElement = document.createElement("li");
  messageElement.textContent = message;
  element.appendChild(messageElement);
}

然后通过全局document对象上的getElementById方法从DOM上获取了一个元素:

const messageElement = document.createElement("li");

这段代码后紧跟着对函数addMessage的调用:

addMessage(first,"Page loading");

这条代码创建了一个新的li元素,然后修改了其中的文字内容,最后将其插入DOM中。
image.png

事件处理

客户端Web应用是一种GUI应用,也就是说这种应用会对不同类型的时间作相应,如鼠标移动、单击和键盘按压等。因此,在页面构建阶段执行的JavaScript代码,除了会影响全局应用状态和修改DOM外,还会注册事件监听器(或处理器)。这类监听器会在事件发生时,由浏览器调用执行。有了这些事件处理器,我们的应用也就有了交互能力。

事件处理器概览

浏览器执行环境的核心思想基于:同一时刻只能执行一个代码片段,即所谓的单线程执行模式。浏览器还会使用事件队列,来跟踪以及发生但尚未处理的事件。
所有已生成的时间(无论是用户生成的,例如鼠标移动或键盘按压,还是服务器生成的,例如Ajax事件)都会放在同一个事件队列中,以它们被浏览器检测到的顺序排序。

事件处理的过程可以描述为一个简单的流程图:

  • 浏览器检查事件队列头;
  • 如果浏览器没有在对队列中检测到事件,则继续检查;
  • 如果浏览器在队列头中检测到了事件,则取出该事件并执行相应的事件处理器(如果存在)。在这个过程中,余下的事件在事件队列中耐心等待,直到轮到它们被处理。

由于一次只能处理一个事件,所以我们必须额外注意处理所有事件的总时间。执行需要花费大量时间执行的事件处理函数会导致Web应用无响应。
image.png
重点注意浏览器在这个过程中的机制,其放置事件的队列是在页面构建和事件处理阶段以外的。这个过程对于决定事件何时发生并推入事件队列很重要,这个过程不会参与事件处理线程。

事件是异步的

事件可能会议难以预计的时间和顺序发生(强制用户以某个顺序按键或单击是非常奇怪的)。我们对事件的处理,以及处理函数的调用是异步的。如下类型的事件会在其他类型事件中发生:

  • 浏览器事件:例如单页面加载完成后无法加载时;
  • 网络事件:例如来自服务器的响应(Ajax事件和服务器端事件);
  • 用户事件:例如鼠标单击、鼠标移动和键盘事件;
  • 计时器事件:但timeout时间到期或又触发了一次时间间隔。

代码的提前建立是为了在之后某个时间点执行。除了全局代码,页面中大部分代码将作为某个事件的结果执行。

注册事件处理器

事件处理器是当某个特定事件发生后我们希望执行的函数。为了达到这个目标,我们必须告知浏览器我们要处理哪个事件。这个过程叫做注册事件处理器。
**
在客户端Web应用中,有两种方式注册事件:

  • 通过把函数赋给某个特殊属性;
  • 通过使用内置addEventListener方法。

例如,把一个函数赋值给window对象上的某个特定属性onload:

window.onload = function() {}

通过这种方式,事件处理器就会注册到load事件上(当DOM已经就绪并全部构建完成,就会触发这个事件)。
类似的,如果我们想要为文档中body元素的单击事件注册处理器:

document.body.onclick = function() {}

把函数赋值给特殊属性是一种简单而直接的注册事件处理方式。但是,这种做法会带来缺点:对于某个事件只能注册一个事件处理器。因此,我们可以用addEventListener方法注册尽可能多的事件。

document.body.addEventListener("mousemove",function(){
  const second = document.getElementById("second");
  addMessage(second,"Event,mousemove");
})

document.body.addEventListener("click",function(){
  const second = document.getElementById("second");
  addMessage(second,"Event,click");
})

本例中使用了某个HTML元素上的内置的方法addEventListener,并在函数中制定了事件的类型(mousemove事件或click事件)和事件处理器。这意味着单鼠标从页面上移动后,浏览器就会添加一条信息到id为second的list元素上:”Event,mousemove”,类似但body被单击后,”Event,click”也会被添加到同样的元素上。

处理事件

事件处理背后的主要思想是:当事件发生时,浏览器调用相应的事件处理器。
由于单线程执行模型,所以同一时刻只能处理一个事件。任何后面的事件都只能在当前事件处理器完全结束执行后才能被处理。

实例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Application Lifecycle</title>
    <style>
        #first{
            color: green;
        }
        #second{
            color: red;
        }
    </style>
</head>
<body>
    <ul id="first"></ul>

    <script>
        function addMessage(element,message){
            const messageElement = document.createElement("li");
            messageElement.textContent = message;
            element.appendChild(messageElement);
        }

        const first = document.getElementById("first");
        addMessage(first,"Page loading");
    </script>

    <ul id="second"></ul>

    <script>
        document.body.dEventListener("mousemove",function(){
            const second = document.getElementById("second");
            addMessage(second,"Event,mousemove");
        })

        document.body.addEventListener("click",function(){
            const second = document.getElementById("second");
            addMessage(second,"Event,click");
        })
    </script>

</body>
</html>

image.png
为了响应用户的动作,浏览器把鼠标移动和单击事件以它们发生的次序放入事件队列:第一个是鼠标移动事件,第二个是单击事件。
在事件处理阶段中,事件循环会检查队列,其发现队列的前面有一个鼠标移动事件,然后执行了相应的事件处理器。当鼠标移动事件处理器处理完毕后,轮到等待在队列中的单击事件。
当鼠标移动事件处理器函数的最后一行代码执行完毕后,JavaScript引擎退出事件处理器函数,鼠标移动事件完整地处理了。
事件循环再次检查队列,这一次在队列的最前面,事件循环发现了鼠标单击事件并处理了该事件。一旦单击处理器执行完成,队列中不再有新的事件,事件循环就会继续循环,等待处理新到来的事件。这个循环会一直执行到用户关闭了Web应用。
image.png