一个 web 页面的生命周期

简单来说,一个 web 页面的生命周期,可以从用户输入 url 并点击访问开始(1⃣️)。浏览器发出请求给对应的服务端(2⃣️),服务端接收到请求后处理响应内容,并发送响应(3⃣️),浏览器收到服务端的返回请求(4⃣️),此时我们的页面才正式进入渲染阶段。

web 页面的渲染由两部分构成:

  1. 页面构建 —— 构建UI内容
  2. 事件处理 —— 浏览器建立起事件循环机制(5⃣️),等待事件的发生,并调用相关事件的处理函数(6⃣️)

页面的生命周期终止于用户关闭页面(7⃣️)。

页面构建阶段

页面构建阶段最主要目标是生成 UI 界面,这由两个步骤实现:

  1. 解析 HTML 并构建 DOM
  2. 执行 JavaScript 代码

步骤 1 由浏览器解析 HTML 节点时执行,步骤 2 是当解析到 HTML 的 script 元素时执行。

在页面构建阶段,浏览器可能多次在这两个步骤之间互相切换。

解析 HTML 并构建 DOM 节点

页面构建从浏览器接收到 HTML 代码开始,这是浏览器构建 UI 界面的基础。

接着浏览器便开始解析 HTML 代码,依次解析每个 HTML 元素,并构建相应的 DOM 节点。

需要注意的是,尽管 HTML 的节点和 DOM 节点非常相似,但是他们并不是同一个东西。你可以将 HTML 想象成是浏览器用来构建 DOM 节点的大纲,浏览器依据它来构建 DOM,并且甚至可以修复某些 HTML 节点的错误。

当进行页面构建时,浏览器可能会遇到一个特殊的 HTML 节点:script,用于包含 JavaScript 代码。到这种情况发生时,浏览器会暂停 DOM 的构建,并开始执行 JavaScript 代码。

执行 JavaScript 代码

所有的 JavaScript 代码都会由浏览器的 JavaScript 引擎所执行,例如:Chrome 的 V8。

JavaScript 的全局变量

浏览器暴露给 JavaScript 引擎最主要的全局对象就是 window。全局对象中最重要的一个属性是 document,代表了当前页面的 DOM。

JavaScript 代码的不同类型

我们可以粗略地将 JavaScript 代码分为两种不同的类型,一种是全局的 JavaScript 代码,另一种是函数内的 js 代码。

1
2
3
4
5
6
7
8
9
10
11
<script>
function addMessage(element, message){
var messageElement = document.createElement("li");
messageElement.textContent = message;
element.appendChild(messageElement);
} // Function code is the code contained in a function.

// Global code is the code outside functions.
var first = document.getElementById("first");
addMessage(first, "Page loading");
</script>

这两种类型的 JavaScript 代码区别在于当他们执行时。全局的代码会被 JavaScript 引擎自上而下地顺序执行,而函数内的代码是在被调用的时候才执行。

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

当浏览器遇到 script 节点时,他会暂停 DOM 的构建,转而开始运行 JavaScript 代码。

所以在执行 script 里的 JavaScript 代码时,只能通过 document 访问并操作到 script 标签之前的 DOM 节点,在这个标签之后的 DOM 节点还未被浏览器所解析,所以不能访问到。

所有用户定义的全局变量都能够在其他的 script 标签中访问到。这是因为这些变量都储存在全局的 window 对象当中,这个 window 对象在页面的生命周期中会一直有效。

这两个步骤:

  1. 从 HTML 中构建出 DOM 节点
  2. 执行 JavaScript 代码
    在页面构建阶段会不停地重复,只要还有 HTML 元素需要处理或者 JavaScript 代码需要执行。

最终,浏览器解析完了所有的 HTML 元素和所有的 JavaScript 代码。浏览器便开始进入页面生命周期的第二阶段:事件处理阶段

事件处理

Web 应用需要对用户的鼠标移动、点击和键盘按下等事件做响应,所以当 JavaScript 执行代码时,也会注册响应的事件处理函数。

事件处理概览

我们知道,JavaScript 是单线程的,我们在一个时间点只能执行一段 JavaScript 代码。所以浏览器需要一种方式记录那些此刻发生的却不需要马上处理的事件,浏览器使用事件队列来实现这一功能。

所有的事件(无论是诸如鼠标点击等的用户事件,或是 ajax 请求等浏览器发出的事件)都会被放置到同一个事件队列当中,以浏览器中触发的顺序被放置。事件处理的过程可以简单地概括为:

  1. 浏览器检查事件队列
  2. 如果事件队列中没有任何事件,那么浏览器会继续检查,不断地循环这个动作
  3. 如果事件队列中有一个事件,那么浏览器会将其取出并执行它所对应的处理函数(如果有的话)。在执行的过程中,队列中其余的事件耐心地等待它的完成。

正因为一次只会有一个事件被执行,所以我们需要格外地注意那些需要长时间运行的事件。

值得注意的是,浏览器将事件推到事件队列中的这一步骤是独立于页面的构建和事件的处理阶段的。

事件是异步的

这几类事件可能在各自的执行阶段中发生:

  1. 浏览器事件,诸如:页面完成加载或当它将被卸载时
  2. 网络事件,如从服务端返回请求内容(Ajax 事件、服务端事件)
  3. 用户事件,鼠标点击、鼠标移动、键盘按下
  4. 计时器事件,setTimeout等函数触发

注册事件处理函数

在浏览器端,有两种注册事件处理函数的方式:

  1. 将函数赋值给特定的属性
  2. 通过内置的 addEventListener 方法

第一种方式:

1
2
3
window.onload = function(){};

document.body.onclick = function(){};

这种方式一般不被推荐,因为这样只能给一个事件注册一个处理函数。

而通过 addEventListener 方法,我们可以按照我们的需要,注册多个事件处理函数。

处理事件