javascript-加载JavaScript的方式

Failure is the condiment that gives success its flavor.
Truman Capote

本系列关注前端部分,根据学习路线图达到学习Vue.js的目的

developer路线图developer-roadmap/translations/chinese at master · kamranahmedse/developer-roadmap

本系列笔记的源代码GitHub项目地址:learning-area/front-end-learning at master · YJ2CS/learning-area

快速跳转

目录:前端学习路线-目录

可以像添加 CSS 那样将 JavaScript 添加到 HTML 页面中。CSS 使用 <link> 元素链接外部样式表,使用 <style> 元素向 HTML 嵌入内部样式表,JavaScript 这里只需一个元素——<script>。我们来看看它是怎么工作的。

内部 JavaScript

首先,下载示例文件 apply-javascript.html。放在一个好记的文件夹里。分别在浏览器和文本编辑器中打开这个文件。你会看到这个 HTML 文件创建了一个简单的网页,其中有一个可点击按钮。然后转到文本编辑器,在 </body> 标签结束前插入以下代码:

1
2
3
4
5
<script>

// 在此编写 JavaScript 代码

</script>

下面,在 <script> 元素中添加一些 JavaScript 代码,这个页面就能做一些更有趣的事。在“/ /在此编写 JavaScript 代码”一行下方添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
document.addEventListener("DOMContentLoaded", function() {
function createParagraph() {
let para = document.createElement('p');
para.textContent = '你点击了这个按钮!';
document.body.appendChild(para);
}

const buttons = document.querySelectorAll('button');

for(let i = 0; i < buttons.length ; i++) {
buttons[i].addEventListener('click', createParagraph);
}
});

保存文件并刷新浏览器,然后你会发现,点击按钮文档下方将会添加一个新段落。注: 如果示例不能正常工作,请依次检查所有步骤,并保证没有纰漏。原始文件是否以 .html 为扩展名保存到本地了?</body> 标签前是否添加了 <script> 元素?JavaScript 代码输入是否正确 ? JavaScript 是区分大小写的,而且非常精确,所以你需要准确无误地输入所示的句法,否则可能会出错。

注: 你可以在 GitHub 上查看此版本 apply-internal.html (也可在线查看)。

外部 JavaScript

这很不错,但是能不能把 JavaScript 代码放置在一个外部文件中呢?现在我们来研究一下。

首先,在刚才的 HTML 文件所在的目录下创建一个名为 script.js 的新文件。请确保扩展名为 .js,只有这样才能被识别为 JavaScript 代码。将 <script> 元素替换为:

1
<script src="script.js" async></script>

在 script.js 文件中,添加下面的脚本:

1
2
3
4
5
6
7
8
9
10
11
function createParagraph() {
let para = document.createElement('p');
para.textContent = '你点击了这个按钮!';
document.body.appendChild(para);
}

const buttons = document.querySelectorAll('button');

for(let i = 0; i < buttons.length ; i++) {
buttons[i].addEventListener('click', createParagraph);
}

保存并刷新浏览器,你会发现二者完全一样。但是现在我们把 JavaScript 写进了一个外部文件。这样做一般会使代码更加有序,更易于复用,且没有了脚本的混合,HTML 也会更加易读,因此这是个好的习惯。注:你可以在 GitHub 上查看这个版本 apply-external.html 以及 script.js (也可在线查看).

内联 JavaScript 处理器

请不要这样做。
注意,有时候你会遇到在 HTML 中存在着一丝真实的 JavaScript 代码。它或许看上去像这样:

1
2
3
4
5
6
function createParagraph() {
const para = document.createElement('p');
para.textContent = '你点击了这个按钮!';
document.body.appendChild(para);
}
<button onclick="createParagraph()">点我呀</button>

你可以在下面尝试这个版本的 demo。

这个 demo 与之前的两个功能完全一致,只是在 <button> 元素中包含了一个内联的 onclick 处理器,使得函数在按钮被按下时运行。

**然而请不要这样做。 ** 这将使 JavaScript 污染到 HTML,而且效率低下。对于每个需要应用 JavaScript 的按钮,你都得手动添加 onclick="createParagraph()" 属性。

可以使用纯 JavaScript 结构来通过一个指令选取所有按钮。下文的这段代码即实现了这一目的:

1
2
3
4
5
const buttons = document.querySelectorAll('button');

for(let i = 0; i < buttons.length ; i++) {
buttons[i].addEventListener('click', createParagraph);
}

这样写乍看去比 onclick 属性要长一些,但是这样写会对页面上所有按钮生效,无论多少个,或添加或删除,完全无需修改 JavaScript 代码。

注:请尝试修改 apply-javascript.html 以添加更多按钮。刷新后可发现按下任一按钮时都会创建一个段落。很高效吧。

脚本调用策略

要让脚本调用的时机符合预期,需要解决一系列的问题。这里看似简单,实际大有文章。最常见的问题就是:HTML 元素是按其在页面中出现的次序调用的,如果用 JavaScript 来管理页面上的元素(更精确的说法是使用 文档对象模型 DOM),若 JavaScript 加载于欲操作的 HTML 元素之前,则代码将出错。

在上文的“内部”、“外部”示例中,JavaScript 调用于文档头处,解析 HTML 文档体之前。这样做是有隐患的,需要使用一些结构来避免错误发生。

“内部”示例使用了以下结构:

1
2
3
document.addEventListener("DOMContentLoaded", function() {
. . .
});

这是一个事件监听器,它监听浏览器的"DOMContentLoaded"事件,即 HTML 文档体加载、解释完毕事件。事件触发时将调用 " . . ." 处的代码,从而避免了错误发生(事件 的概念稍后学习)。

“外部”示例中使用了 JavaScript 的一项现代技术(async “异步”属性)来解决这一问题,它告知浏览器在遇到 <script> 元素时不要中断后续 HTML 内容的加载。

1
<script src="script.js" async></script>

上述情况下,脚本和 HTML 将一并加载,代码将顺利运行。

注:“外部”示例中 async 属性可以解决调用顺序问题,因此无需使用 DOMContentLoaded 事件。而 async 只能用于外部脚本,因此不适用于“内部”示例。

解决此问题的旧方法是:把脚本元素放在文档体的底端(</body> 标签之前,与之相邻),这样脚本就可以在 HTML 解析完毕后加载了。此方案(以及上述的 DOMContentLoaded 方案)的问题是:只有在所有 HTML DOM 加载完成后才开始脚本的加载/解析过程。对于有大量 JavaScript 代码的大型网站,可能会带来显著的性能损耗。这也是 async 属性诞生的初衷。

async 和 defer

上述的脚本阻塞问题实际有两种解决方案 —— async 和 defer。我们来依次讲解。

浏览器遇到 async 脚本时不会阻塞页面渲染,而是直接下载然后运行。

这样脚本的运行次序就无法控制,只是脚本不会阻止剩余页面的显示。

当页面的脚本之间彼此独立,且不依赖于本页面的其它任何脚本时,async 是最理想的选择。

比如,如果你的页面要加载以下三个脚本:

1
2
3
4
5
<script async src="js/vendor/jquery.js"></script>

<script async src="js/script2.js"></script>

<script async src="js/script3.js"></script>

三者的调用顺序是不确定的。jquery.js 可能在 script2 和 script3 之前或之后调用,如果这样,后两个脚本中依赖 jquery 的函数将产生错误,因为脚本运行时 jquery 尚未加载。

解决这一问题可使用 defer 属性,脚本将按照在页面中出现的顺序加载和运行:

1
2
3
4
5
<script defer src="js/vendor/jquery.js"></script>

<script defer src="js/script2.js"></script>

<script defer src="js/script3.js"></script>

添加 defer 属性的脚本将按照在页面中出现的顺序加载,因此第二个示例可确保 jquery.js 必定加载于 script2.js 和 script3.js 之前,同时 script2.js 必定加载于 script3.js 之前。

脚本调用策略小结:

如果脚本无需等待页面解析,且无依赖独立运行,那么应使用 async。如果脚本需要等待页面解析,且依赖于其它脚本,调用这些脚本时应使用 defer,将关联的脚本按所需顺序置于 HTML 中。