JavaScipt 忍者秘籍之运行时的页面构建过程

Posted king2019blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScipt 忍者秘籍之运行时的页面构建过程相关的知识,希望对你有一定的参考价值。

生命周期概览

典型客户端Web应用的生命周期从用户在浏览器地址栏输入一串URL,或单击一个链接开始:

技术图片

从用户的角度来说:

? 浏览器构建了发送至服务器(序号2)的请求,该服务器处理了请求(序号3)并形成了一个通常由html、CSS和javascript代码所组成的响应;

? 当浏览器接收了响应(序号4)时,我们的客户端应用开始了它的生命周期。

? 由于客户端Web应用是图形用户界面(GUI)应用,其生命周期与其他的GUI应用相似(例如标准的桌面应用或移动应用),其执行步骤如下所示:

? 1.页面构建——创建用户界面;

? 2.事件处理——进入循环(序号5)从而等待事件(序号6)的发生,发生后调用事件处理器。

? 应用的生命周期随着用户关掉或离开页面(序号7)而结束.

<!DOCTYPE html>
<html>
    <head>
        <title>Web app lifecycle</title>
        <style>
            #first  color: green;
            #second  color: red;
        </style>
    </head>
    <body>
        
        <ul id="first"> </ul>
        
        <script>
            function addMessage(element, message)
                var messageElement = document.createElement("li");
                messageElement.textContent = message;
                element.appendChild(messageElement);
             //<--- 定义一个函数用于向一个元素增加一条信息
            var first = document.getElementById("first");
            addMessage(first, "Page loading");
        </script>
        <ul id="second"> </ul>
        <script>
            document.body.addEventListener("mousemove", function() 
                //---为body附上鼠标移动事件处理函数
                var second = document.getElementById("second");
                addMessage(second, "Event: mousemove");
            );
                //为body附上鼠标点击事件处理函数
            document.body.addEventListener("click", function() 
                var second = document.getElementById("second");
                addMessage(second, "Event: click");
            );
    </script>
    </body>
</html>                                              

? 定义了两条CSS 规则,即#first和#second,其指定了ID为first和second两个元素的文字颜色(从而使我们方便地区分两者)。随后用first这个id定义了一个列表元素:

<ul id="first"></ul>

? 然后定义一个addMessage函数,每当调用该函数都会创建一个新的表项元素,为其设置文字内容,然后将其附加到一个现有的元素上:

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

? 通过使用内置的方法getElementById来从文档中获取ID为first的元素,然后为该元素添加一条信息,用于告知页面正在加载中:

var first = document.getElementById("first");
addMessage(first, "Page loading");

? 然后我们又定义了一个列表元素,这次给该列表赋予的ID属性为second:

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

? 最后将这两个事件处理器附加到Web页面的body上。每当用户移动鼠标,鼠标移动事件处理器就会被执行,然后该处理器调用addMessage方法,为第二个列表元素加上一句话“Event: mousemove”。

document.body.addEventListener("mousemove", function() 
    var second = document.getElementById("second");
    addMessage(second, "Event: mousemove");
);

? 还注册了一个单击事件处理器,每当用户单击页面就会输出该消息“Event: click”,并添加至第二个列表元素中。

document.body.addEventListener("click", function()
    var second = document.getElementById("second");
    addMessage(second, "Event: click");
);

页面构建阶段

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

? 1.解析HTML代码并构建文档对象模型 (DOM);

? 2.执行JavaScript代码。

技术图片

HTML解析和DOM构建

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

技术图片

? 注意图中的节点是如何组织的,除了第一个节点——html根节点(序号1)以外,所有节点都只有一个父节点。

? 例如,head节点(序号2)父节点为html节点(序号1)。同时,一个节点可以有任意数量的子节点。例如,html节点(序号1)有两个孩子节点:head节点(序号2)和body节点。同一个元素的孩子节点被称作兄弟节点。(head节点和body节点是兄弟节点)尽管DOM是根据HTML来创建的,两者紧密联系,但需要强调的是,它们两者并不相同。你可以把HTML代码看作浏览器页面UI构建初始DOM的蓝图。为了正确构建每个DOM,浏览器还会修复它在蓝图中发现的问题。让我们看下面的示例,如图所示。

技术图片

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


执行JavaScript代码

JavaScript中的全局对象

? 浏览器暴露给JavaScript 引擎的主要全局对象是window对象,它代表了包含着一个页面的窗口。

? window对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器API的访问途径。

? 全局window对象最重要的属性是document,它代表了当前页面的DOM。

? 通过使用这个对象,JavaScript代码就能在任何程度上改变DOM,包括修改或移除现存的节点,以及创建和插入新的节点。

var first = document.getElementById("first");

? 使用全局document对象来通过ID选择一个元素,然后将该元素赋值给变量first。

? 随后我们就能在该元素上用JavaScript代码来对其作各种操作,例如改变其文字内容,修改其属性,动态创建和增加新孩子节点,甚至可以从DOM上将该元素移除。

JavaScript代码的不同类型

? 区分出两种不同类型的JavaScript代码:全局代码函数代码

<script>
    function addMessage(element, message)
        var messageElement = document.createElement("li");
        messageElement.textContent = message;  //--- 函数代码指的是包含在函数中的代码
        element.appendChild(messageElement);
    
    var first = document.getElementById("first");
    addMessage(first, "Page loading"); //--- 全局代码指的是位于函数之外的代码
</script>

? 这两类代码的主要不同是它们的位置:包含在函数内的代码叫作函数代码,而在所有函数以外的代码叫作全局代码。

? 全局代码由JavaScript引擎(后续会做更多解释)以一种直接的方式自动执行,每当遇到这样的代码就一行接一行地执行。

? 例如,定义addMessage函数的全局代码片段使用内置方法getElementById来获取ID为first的元素,然后再调用addMessage函数,如图所示,每当遇到这些代码就会一个个执行。

技术图片

? 反过来,若想执行函数代码,则必须被其他代码调用:既可以是全局代码(例如,由于全局代码的执行过程中执行了addMessage函数代码,所以addMessage函数被调用),也可以是其他函数,还可以由浏览器调用。

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

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

技术图片

? 在全局JavaScript代码被执行后DOM的状态。让我们仔细看看这个执行过程。首先定义了一个addMessage函数:

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

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

var first = document.getElementById("first");

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

addMessage(first, "Page loading");

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

? 这个例子中,JavaScript通过创建一个新元素并将其插入DOM节点修改了当前的DOM结构。一般来说,JavaScript 代码能够在任何程度上修改DOM结构:

? 它能创建新的节点或移除现有DOM节点。

? 但它依然不能做某些事情,例如选择和修改还没被创建的节点。

? 这就是为什么要把script元素放在页面底部的原因。如此一来,我们就不必担心是否某个HTML元素已经加载为DOM。

? 一旦JavaScript引擎执行到了脚本元素中(如上图中的addMessage函数返回)JavaScript代码的最后一行,浏览器就退出了JavaScript执行模式,并继续余下的HTML构建为DOM节点。在这期间,如果浏览器再次遇到脚本元素,那么从HTML到DOM的构建再次暂停,JavaScript运行环境开始执行余下的JavaScript代码。

? 需要重点注意:

? JavaScript应用在此时依然会保持着全局状态。

? 所有在某个JavaScript代码执行期间用户创建的全局变量都能正常地被其他脚本元素中的JavaScript代码所访问到。

? 其原因在于全局window对象会存在于整个页面的生存期之间,在它上面存储着所有的JavaScript变量。

? 只要还有没处理完的HTML元素和没执行完的JavaScript代码,下面两个步骤都会一直交替执行。

? 1.将HTML构建为DOM。
? 2.执行JavaScript代码。

? 最后,当浏览器处理完所有HTML元素后,页面构建阶段就结束了


事件处理

事件处理器概览

浏览器执行环境的核心思想基于:==同一时刻只能执行一个代码片段,即所谓的单线程执行模型。==

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

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

由于一次只能处理一个事件,所以我们必须格外注意处理所有事件的总时间。执行需要花费大量时间执行的事件处理函数会导致Web应用无响应!

技术图片

? 重点注意浏览器在这个过程中的机制,其放置事件的队列是在页面构建阶段和事件处理阶段以外的。

? 这个过程对于决定事件何时发生并将其推入事件队列很重要,这个过程不会参与事件处理线程。


事件是异步的

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

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

? 事件处理的概念是Web应用的核心,代码的提前建立是为了在之后的某个时间点执行。除了全局代码,页面中的大部分代码都将作为某个事件的结果执行。
? 在事件能被处理之前,代码必须要告知浏览器我们要处理特定事件.


注册事件处理器

? 事件处理器是==当某个特定事件发生后我们希望执行的函数。==为了达到这个目标,我们必须告知浏览器我们要处理哪个事件。这个过程叫作注册事件处理器

? 在客户端Web应用中,有两种方式注册事件。

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

? 将一个函数赋值给window对象上的某个特定属性onload:

window.onload = function();

? 通过这种方式,事件处理器就会注册到load事件上(当DOM已经就绪并全部构建完成,就会触发这个事件)。

? 如果我们想要为在文档中body元素的单击事件注册处理器,我们可以输入下述代码:

document.body.onclick = function();

? 把函数赋值给特殊属性是一种简单而直接的注册事件处理器方式。

? 但是,我们并不推荐你使用这种方式来注册事件处理器,这是因为这种做法会带来缺点:对于某个事件只能注册一个事件处理器。也就是说,一不小心就会将上一个事件处理器改写掉。

? 幸运的是,还有一种替代方案:addEventListener方法让我们能够注册尽可能多的事件,只要我们需要.

<script>
    //--- 为mousemove事件注册处理器
    document.body.addEventListener("mousemove", function()  
        var second = document.getElementById("second");
        addMessage(second, "Event: mousemove");
    );
    //-- 为click事件注册处理器
    document.body.addEventListener("click", function() 
        var second = document.getElementById("second");
        addMessage(second, "Event: click");
    );
</script>

? 本例中使用了某个HTML元素上的内置的方法addEventListener,并在函数中指定了事件的类型(mousemove事件或click)和事件的处理器。

? 这意味着当鼠标在页面上移动后,浏览器会调用该函数添加一条消息到ID为second的list元素上,"Event: mousemove"(类似,当body被单击时,"Event: click"也会被添加到同样的元素上)。


处理事件

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

展示了在用户快速移动和单击鼠标时的执行情况。

技术图片

? 为了响应用户的动作,浏览器把鼠标移动和单击事件以它们发生的次序放入事件队列:

  1. 第一个是鼠标移动事件,第二个是单击事件序号1。
  2. 在事件处理阶段中,事件循环会检查队列,其发现队列的前面有一个鼠标移动事件,然后执行了相应的事件处理器序号2。
  3. 当鼠标移动事件处理器处理完毕后,轮到了等待在队列中的单击事件。当鼠标移动事件处理器函数的最后一行代码执行完毕后,JavaScript引擎退出事件处理器函数,鼠标移动事件完整地处理了序号3,事件循环再次检查队列。这一次,在队列的最前面,事件循环发现了鼠标单击事件并处理了该事件。
  4. 一旦单击处理器执行完成,队列中不再有新的事件,事件循环就会继续循环,等待处理新到来的事件。
  5. 这个循环会一直执行到用户关闭了Web应用。

技术图片

  1. 执行鼠标移动处理器时会选择第二个列表元素,其ID为second。
  2. 然后通过使用addMessage,使用文字“Event: mousemove”添加了一个新的列表项元素序号1。
  3. 一旦鼠标移动处理器结束后,事件循环执行单击事件处理器,从而创建了另一个列表元素序号2,并附加在ID为second的第二个列表元素后。

小结

  1. 浏览器接收的HTML代码用作创建DOM的蓝图,它是客户端Web应
    用结构的内部展示阶段。

  2. 我们使用JavaScript代码来动态地修改DOM以便给Web应用带来动
    态行为。

  3. 客户端Web应用的执行分为两个阶段:

    ? ==页面构建代码是用于创建DOM的,而全局JavaScript代码是遇到script节点时执行的。==在这个执行过程中,JavaScript代码能够以任意程度改变当前的DOM,还能够注册事件处理器——事件处理器是一种函数,当某个特定事件(例如,一次鼠标单击或键盘按压)发生后会被执行。注册事件处理器很容易:使用内置的addEventListener方法。

    ? ==事件处理——在同一时刻,只能处理多个不同事件中的一个,处理顺序是事件生成的顺序。==事件处理阶段大量依赖事件队列,所有的事件都以其出现的顺序存储在事件队列中。事件循环会检查事件队列的队头,如果检测到了一个事件,那么相应的事件处理器就会被调用。

以上是关于JavaScipt 忍者秘籍之运行时的页面构建过程的主要内容,如果未能解决你的问题,请参考以下文章

求忍者神龟2过关秘籍

JavaScript忍者秘籍

第二章 运行时的页面构建过程

JavaScript忍者秘籍--第一章笔记

我为什么要推荐《JavaScript 忍者秘籍(第2版)》

忍者和 npm_executable-notfound