注:此篇文章适合有一定JavaScript基础的同学食用,非零基础

生命周期概览

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

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

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

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

页面构建阶段

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

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

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

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

当浏览器遇到第一个脚本元素时,它已经用多个HTML元素创建了一个简单的DOM树。

附:

HTML5规范:https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5

DOM3规范:https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model

需要注意的是,每当解析到脚本元素时,浏览器就会停止从HTML构建DOM,并开始执行JavaScript代码,并不是一起进行构建,因为JavaScript中可能会有对DOM进行操作,如果同时进行构建可能会引发一些不必要的错误。

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

页面构建一小时,适配就搞一天。

好消息是微软即将停止对IE的支持,IE将退出历史舞台,不必再费心费力的去考虑IE用户

JavaScript中的全局对象

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

浏览器API:

https://developer.mozilla.org/en-US/docs/Web/API

JavaScript代码的不同类型

  • 全局代码
  • 函数代码

比如:

包含在函数内的代码叫作函数代码,而在所有函数以外的代码叫作全局代码,全局代码由JavaScript引擎以一种直接的方式自动执行,每当遇到这样的代码就一行接一行地执行

若想执行函数代码,则必须被其他代码调用:既可以是全局代码,也可以是其他函数,还可以由浏览器调用。

事件处理

客户端Web应用是一种GUI应用,也就是说这种应用会对不同类型的事件作响应

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

当一个事件抵达后,浏览器需要执行相应的事件处理函数。这里不保证用户总会极富耐心地等待很长时间,直到下一个事件触发。所以,浏览器需要一种方式来跟踪已经发生但尚未处理的事件。为实现这个目标,浏览器使用了事件队列

所有已生成的事件(无论是用户生成的,例如鼠标移动或键盘按压,还是服务器生成的,例如Ajax事件)都会放在同一个事件队列中,以它们被浏览器检测到的顺序排列

事件处理的过程:

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

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

重点注意浏览器在这个过程中的机制,其放置事件的队列是在页面构建阶段和事件处理阶段以外的。这个过程对于决定事件何时发生并将其推入事件队列很重要,这个过程不会参与事件处理线程

事件是异步的

事件可能会以难以预计的时间和顺序发生(强制用户以某个顺序按键或单击是非常奇怪的)

我们对事件的处理,以及处理函数的调用是异步的

如下类型的事件会在其他类型事件中发生

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

事件处理的概念是Web应用的核心,除了全局代码,页面中的大部分代码都将作为某个事件的结果执行

在事件能被处理之前,代码必须要告知浏览器我们要处理特定事件

注册事件

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

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

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

例如:

1
window.onload = function () {};

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

把函数赋值给特殊属性是一种简单而直接的注册事件处理器方式,但是致命的缺点是对于某个事件只能注册一个事件处理器

addEventListener 方法让我们能够注册尽可能多的事件

例如:

1
2
3
4
5
6
7
8
document.body.addEventListener ("mousemove", function () {
var second = document.getElementById("second");
addMessage (second, "Event: mousemove";)
});
document.body.addEventListener ("click", function () {
var second = document.getElementById("second");
addMessage (second, "Event: click";)
});

当鼠标移动或者单击body时,两个事件就会被触发,添加一条消息到id为second的元素上

处理事件

事件处理背后的主要思想是当事件发生时,浏览器调用相应的事件处理器

由于单线程执行模型,所以同一时刻只能处理一个事件,任何事件只能等待当前事件被处理完毕之后才会被执行

在上面的例子中,浏览器的动作是这样的:

鼠标移动和单击的事件处理阶段

小结

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

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

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

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

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

函数:定义与参数

像普通人一样编写代码和像“忍者”一样编写代码的最大差别在于是否把JavaScript作为函数式语言(functional language)来理解。对这一点的认知水平决定了你编写的代码水平。

——《JavaScript忍者秘籍》

JavaScript中最关键的概念是:函数是第一类对象(first-class objects),或者说它们被称作一等公民(first-class citizens)

函数与对象共存,函数也可以被视为其他任意类型的JavaScript对象,函数和那些更普通的JavaScript数据类型一样,它能被变量引用,能以字面量形式声明,甚至能被作为函数参数进行传递

函数及函数式概念之所以如此重要,其原因之一在于函数是程序执行过程中的主要模块单元。除了全局JavaScript代码是在页面构建的阶段执行的,我们编写的所有的脚本代码都将在一个函数内执行

JavaScript中对象有以下几种常用功能:

  • 对象可通过字面量来创建{}

  • 对象可以赋值给变量、数组项,或其他对象的属性

  • 对象可以作为参数传递给函数

  • 对象可以作为函数的返回值

  • 对象能够具有动态创建和分配的属性

不同于很多其他编程语言,在JavaScript中,我们几乎能够用函数来实现同样的事

函数是第一类对象

JavaScript中函数拥有对象的所有能力,也因此函数可被作为任意其他类型对象来对待。当我们说函数是第一类对象的时候,就是说函数也能够实现以下功能:

  • 通过字面量创建

    1
    function ninjaFunction () {}
  • 赋值给变量,数组项或其他对象的属性

  • 作为函数的参数来传递

  • 作为函数的返回值

  • 具有动态创建和分配的属性

对象能做的任何一件事,函数也都能做。函数也是对象,唯一的特殊之处在于它是可调用的(invokable),即函数会被调用以便执行某项动作

把函数作为第一类对象是函数式编程(functional programming)的第一步,函数式编程是一种编程风格,它通过书写函数式(而不是指定一系列执行步骤,就像那种更主流的命令式编程)代码来解决问题。函数式编程可以让代码更容易测试、扩展及模块化

第一类对象的特点之一是,它能够作为参数传入函数。对于函数而言,这项特性也表明:如果我们将某个函数作为参数传入另一个函数,传入函数会在应用程序执行的未来某个时间点才执行,说的一般一点就是回调函数(callbackfunction)

回调函数

每当我们建立了一个将在随后调用的函数时,无论是在事件处理阶段通过浏览器还是通过其他代码,我们都是在建立一个回调(callback)

回调就是在执行过程中,我们建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”

有效运用JavaScript的关键在于如何使用回调函数

下面这个函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数:

1
2
3
function useless (ninjaCallBack) {
return ninjaCallBack();
}

这个例子只是告诉我们,这个函数完全没什么实际用处,它的参数接收另一个函数的引用,并作为回调调用该函数,

再来看一个例子:

1
2
3
4
5
6
7
8
9
10
function getText () {
report ("in getText function");
return text;
}
// 简单的函数定义,仅返回一个全局变量
report ("before making all the calls");
assert (useless (getText) === text,
"the useless function is working" + text);
report ("after the calls have been made");
// 把getText作为回调函数传入上面的useless函数

断言函数assert通常使用两个参数。第一个参数是用于断言的表达式,本例中,我们需要确定使用参数getText调用useless函数返回的值与变量text是否相等 useless(getText) === text ,若第一个参数的执行结果为true,断言通过;反之,断言失败。第二个参数是与断言相关联的信息,通常会根据通过/失败来输出到日志上

断言函数实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--
* @Author: DaBaiLuoBo
* @Date: 2020-09-02 22:27:45
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="">
function assert (value, desc) {
var li = document.createElement("li");
li.className = value ? "pass" : "fail";
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
// 创建一个assert方法
window.onload = function () {
assert(true, "the test suite is running.");
assert(false, "Fail");
};
// 通过断言执行测试用例
</script>
<style>
#results li.pass {color: green;}
#results li.fail {color: red;}
</style>
<!-- 定义输出结果样式 -->
</head>
<body>
<ul id="results"></ul>
</body>
</html>

自定义函数 report()

1
2
3
function report(text) {
assert (true,text);
}

现在来一个小例子测试一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
var text = 'test test';
function useless (ninjaCallBack) {
return ninjaCallBack();
}
function getText() {
report("in getText function");
return text;
}
report("before making all the calls");
assert(useless(getText) === text,
"the useless function is working" + text);
report("after the calls have been made");

执行结果:

getText参数调用useless回调函数后,得到了期望的返回值,代码执行过程:

JavaScript的函数式本质让我们能把函数作为第一类对象,所以上述例子中的代码可以被简化为:

JavaScript的重要特征之一是可以在表达式出现的任意位置创建函数,除此之外这种方式能使代码更紧凑和易于理解(把函数定义放在函数使用处附近)

当一个函数不会在代码的多处位置被调用时,该特性可以避免用非必须的名字污染全局命名空间

使用比较器排序

一般情况下只要我们拿到了一组数据集,就很可能需要对它进行排序。假如有一组随机序列的数字数组:0, 3, 2, 5, 7, 4, 8, 1

JavaScript数组可以用sort方法。利用该方法可以只定义一个比较算法,比较算法用于指示按什么顺序排列,这才是回调函数所要介入的

不同于让排序算法来决定哪个值在前哪个值在后,我们将会提供一个函数来执行比较。我们会让排序算法能够获取这个比较函数作为回调,使算法在其需要比较的时候,每次都能够调用回调

该回调函数的期望返回值为:

  • 如果传入值的顺序需要被调换,返回正数
  • 不需要调换,返回负数
  • 两个值相等,返回0

对于排序上述数组,我们对比较值做减法就能得到我们所需要的值:

1
2
3
4
var values = [0, 3, 2, 5, 7, 4, 8, 1]
values.sort(function(value1,value2) {
return value1 - value2;
})

没有必要思考排序算法的底层细节(甚至是选择了什么算法)。JavaScript引擎每次需要比较两个值的时候都会调用我们提供的回调函数

函数式方式让我们能把函数作为一个单独实体来创建,正像我们对待其他类型一样,创建它、作为参数传入一个方法并将它作为一个参数来接收,函数就这样显示了它一等公民的地位

函数作为对象的乐趣

可以给函数添加属性:

1
2
3
4
5
6
// 创建一个新的对象并赋予一个新的属性
var ninja = {};
ninja.name = "hitsuke";
// 创建一个函数并赋予一个新的属性
var wieldSword = function () {};
wieldSword.swordType = "katana";

这种特性所能做的更有趣的事:

  • 在集合中存储函数使我们轻易管理相关联的函数,例如,某些特定情况下必须调用的回调函数
  • 记忆让函数能记住上次计算得到的值,从而提高后续调用的性能

今天就到这里,有时间再更新~