1、页面生命周期
DOMContentLoaded
-
DOMContentLoaded—— 浏览器已完全加载 HTML,并构建了 DOM 树,但像<img>和样式表之类的外部资源可能尚未加载完成。-
发生在
document对象上,必须使用addEventListener来捕获:document.addEventListener("DOMContentLoaded", ready); -
可以将 JavaScript 应用于元素,例如查找 DOM 节点。
-
诸如
<script>...</script>之类的脚本会阻塞DOMContentLoaded,浏览器将等待它们执行结束。
<script> document.addEventListener("DOMContentLoaded", () => { alert("DOM ready!"); }); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script> <script> alert("Library loaded, inline script executed"); </script> // 我们会先看到 “Library loaded…”,然后才会看到 “DOM ready!”(所有脚本都已经执行结束)不会阻塞
DOMContentLoaded的脚本此规则有两个例外:
- 具有
async特性(attribute)的脚本不会阻塞DOMContentLoaded。 - 使用
document.createElement('script')动态生成并添加到网页的脚本也不会阻塞DOMContentLoaded。
- 外部样式表不会影响 DOM,因此
DOMContentLoaded不会等待它们。但如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成。当DOMContentLoaded等待脚本时,它现在也在等待脚本前面的样式:
<link type="text/css" rel="stylesheet" href="style.css"> <script> // 在样式表加载完成之前,脚本都不会执行 alert(getComputedStyle(document.body).marginTop); </script>- Firefox,Chrome 和 Opera 都会在
DOMContentLoaded中自动填充表单。
-
load
-
load—— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。<script> window.onload = function() { // 与此相同 window.addEventListener('load', (event) => { alert('Page loaded'); // 此时图片已经加载完成 alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); }; </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0"> -
beforeunload事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。你可以通过运行下面这段代码,然后重新加载页面来进行尝试:
window.onbeforeunload = function() { return false; };由于历史原因,返回非空字符串也被视为取消事件。在以前,浏览器曾经将其显示为消息,但是根据 现代规范 所述,它们不应该这样。
window.onbeforeunload = function() { return "There are unsaved changes. Leave now?"; };它的行为已经改变了,因为有些站长通过显示误导性和恶意信息滥用了此事件处理程序。所以,目前一些旧的浏览器可能仍将其显示为消息,但除此之外 —— 无法自定义显示给用户的消息。
unload
-
unload事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作(只能执行不涉及延迟或询问用户的简单操作),例如发送统计数据。- 我们可以使用
navigator.sendBeacon来发送网络请求。详见规范 w3c.github.io/beacon/。它会在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行sendBeacon。
let analyticsData = { /* 带有收集的数据的对象 */ }; window.addEventListener("unload", function() { navigator.sendBeacon("/analytics", JSON.stringify(analyticsData)); });- 请求以 POST 方式发送。
- 我们不仅能发送字符串,还能发送表单以及其他格式的数据,但通常它是一个字符串化的对象。
- 数据大小限制在 64kb。
当
sendBeacon请求完成时,浏览器可能已经离开了文档,所以就无法获取服务器响应(对于分析数据来说通常为空)。 - 我们可以使用
document.readyState
-
document.readyState属性可以为我们提供当前文档的加载状态。-
loading—— 文档正在被加载。 -
interactive—— 文档被全部读取。- 与
DOMContentLoaded几乎同时发生,但是在DOMContentLoaded之前发生。
- 与
-
complete—— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。- 与
window.onload几乎同时发生,但是在window.onload之前发生。
- 与
-
可以在
readystatechange事件中跟踪状态更改。
// 状态改变时打印它 document.addEventListener('readystatechange', () => console.log(document.readyState)); -
让我们看看完整的事件流。
<script>
log('initial readyState:' + document.readyState);
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
window.onload = () => log('window onload');
</script>
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/451f6107a29f49f29dac999cea336d1b~tplv-k3u1fbpfcp-zoom-1.image" id="img">
<script>
img.onload = () => log('img onload');
</script>
典型输出:
- [1] initial readyState:loading
- [2] readyState:interactive
- [2] DOMContentLoaded
- [3] iframe onload
- [4] img onload
- [4] readyState:complete
- [4] window onload
方括号中的数字表示发生这种情况的大致时间。标有相同数字的事件几乎是同时发生的(± 几毫秒)。
2、<script>元素
<script>元素的属性
<script>元素有下列 8 个属性:
-
src:可选。表示包含要执行的代码的外部文件。- 如果设置了
src特性,script标签的内部代码将会被忽略。
- 如果设置了
-
language:废弃。最初用于表示代码块中的脚本语言(如 "JavaScript" 、"JavaScript 1.2" 或 "VBScript")。大多数浏览器都会忽略这个属性,不应该再使用它。 -
charset:可选。使用src属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值。 -
type:可选。代替language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯例,这个值始终都是"text/javascript"。- 如果这个值是
type = "module",则代码会被当成 ES6 模块,而且只有这时候代码中才能出现import和export关键字。
- 如果这个值是
async
-
async:可选。表示脚本会在“后台”下载,并在加载就绪时执行(加载优先顺序)。-
只对外部脚本文件有效。
-
async脚本不会等待其他脚本(不保证能按照它们出现的次序执行)。 -
异步脚本保证会在页面的
load事件前执行,但可能会在DOMContentLoaded前或之后。
<head> <!-- 第二个脚本可能先于第一个脚本执行 --> <script async src="example1.js"></script> <script async src="example2.js"></script> </head> -
defer
defer:可选。表示脚本会在“后台”下载,然后等 DOM 构建完成后,脚本才会执行。。-
只对外部脚本文件有效。
-
一个页面有多个
defer,按出现的顺序执行。 -
在
DOMContentLoaded事件之前执行。- 在实际当中,推迟执行的脚本不一定总会按顺序执行或者在
DOMContentLoaded事件之前执行(例如脚本包含定时器),因此最好只包含一个这样的脚本。
- 在实际当中,推迟执行的脚本不一定总会按顺序执行或者在
<head> <!-- 第一个脚本执行之后再执行第二个脚本 --> <script defer src="example1.js"></script> <script defer src="example2.js"></script> </head>-
crossorigin 跨源
crossorigin:可选。配置相关请求的CORS(跨源资源共享) 设置。默认不使用CORS。-
一个源(域/端口/协议三者)无法获取另一个源(origin)的内容。
-
要允许跨源访问,
<script>标签需要具有crossorigin特性(attribute),并且远程服务器必须提供特殊的header。这里有三个级别的跨源访问:-
无
crossorigin特性:禁止访问。 -
crossorigin="anonymous"配置文件请求不必设置凭据标志(不会发送 cookie,需要一个服务器端的 header)。-
如果服务器的响应带有包含
*或我们的源(origin)的 headerAccess-Control-Allow-Origin,则允许访问。 -
浏览器不会将授权信息和 cookie 发送到远程服务器。
-
-
crossorigin="use-credentials"设置凭据标志,意味着出站请求会包含凭据(会发送 cookie,需要两个服务器端的 header)。-
如果服务器发送回带有我们的源的 header
Access-Control-Allow-Origin和Access-Control-Allow-Credentials: true,则允许访问。 -
浏览器会将授权信息和 cookie 发送到远程服务器。
-
-
<script> window.onerror = function(message, url, line, col, errorObj) { alert(`${message}\n${url}, ${line}:${col}`); }; </script> <script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script> <!-- 假设服务器提供了 `Access-Control-Allow-Origin` header,则可以正常访问。 -->-
integrity
integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。- 如果接收到的资源的签名与这个属性指定的签名 不匹配,则页面会报错,脚本不会执行。
- 这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容。
默认执行顺序
现代的网站中,脚本往往比 HTML 更“重”:它们的大小通常更大,处理时间也更长。
当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。
这会导致两个重要的问题:
- 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
- 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在脚本下载并执行结束前,用户都不能看到页面内,导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。
为解决这个问题,现代 Web 应用程序通常将所有 JavaScript 引用放在<body>元素中的最后,如下面的例子所示:
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
</head>
<body>
<!-- 这里是页面内容 -->
<script src="example1.js"></script>
<script src="example2.js"></script>
</body>
</html>
这样一来,页面会在处理 JavaScript 代码之前完全渲染页面。用户会感觉页面加载更快了,因为浏览器显示空白页面的时间短了。
但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟。
这对于使用高速连接的人来说,这不值一提,他们不会感受到这种延迟。但是这个世界上仍然有很多地区的人们所使用的网络速度很慢,并且使用的是远非完美的移动互联网连接。
幸运的是,defer 和 async 可以为我们解决这个问题。
动态加载脚本
通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可。
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);
当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的<script>元素是以异步方式加载的,相当于添加了 async 属性。
可以设置 script.async = false,脚本将按照脚本在文档中的顺序执行,就像 defer 那样:
let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script);
以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级。
要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:<link rel="preload" href="gibberish.js">
行内代码与外部文件
虽然可以直接在 HTML 文件中嵌入 JavaScript 代码,但通常认为最佳实践是尽可能将 JavaScript 代码放在外部文件中。不过这个最佳实践并不是明确的强制性规则。推荐使用外部文件的理由如下。
-
可维护性。JavaScript 代码如果分散到很多 HTML 页面,会导致维护困难。而用一个目录保存所有 JavaScript 文件,则更容易维护。
-
缓存。浏览器会根据特定的设置(下载)缓存所有外部链接的 JavaScript 文件,之后,其他页面想要相同的脚本就会从缓存中获取。这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。
-
适应未来。通过把 JavaScript 放到外部文件中,就不必考虑行内
<script>代码不能用于 XHTML ,必须通过一些注释或<script type="text/javascript"><![CDATA[ ... ]]></script>代码块来解决问题。
3、<noscript>元素
<noscript>元素,给不支持 JavaScript 的浏览器提供替代内容。
<noscript>元素可以包含任何可以出现在<body>中的 HTML 元素,<script>除外。
在下列两种情况下,浏览器将显示包含在<noscript>中的内容:
- 浏览器不支持脚本;
- 浏览器对脚本的支持被关闭。
任何一个条件被满足,包含在
<noscript>中的内容就会被渲染。否则,浏览器不会渲染<noscript>中的内容。
<!DOCTYPE html>
<html>
<head>
<title>Example HTML Page</title>
<script defer="defer" src="example1.js"></script>
<script defer="defer" src="example2.js"></script>
</head>
<body>
<noscript>
<p>This page requires a JavaScript-enabled browser.</p>
</noscript>
</body>
</html>
这个例子是在脚本不可用时让浏览器显示一段话。如果浏览器支持脚本,则用户永远不会看到它。
4、资源加载
浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。
这里有两个事件:
-
onload—— 在脚本加载并执行完成时触发。- 在
onload中我们可以使用脚本中的变量,运行函数等
let script = document.createElement('script'); // 可以从任意域(domain),加载任意脚本 script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js" document.head.append(script); script.onload = function() { // 该脚本创建了一个变量 "_" alert( _.VERSION ); // 显示库的版本 }; - 在
-
onerror—— 出现 error。- 无法获取更多 HTTP error 的详细信息。
let script = document.createElement('script'); script.src = "https://example.com/404.js"; // 没有这个脚本 document.head.append(script); script.onerror = function() { alert("Error loading " + this.src); // Error loading https://example.com/404.js };
onload/onerror事件仅跟踪加载本身。在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。也就是说:如果脚本成功加载,则即使脚本中有编程 error,也会触发
onload事件。如果要跟踪脚本 error,可以使用
window.onerror全局处理程序。
load 和 error 事件也适用于其他资源,基本上(basically)适用于具有外部 src 的任何资源。
let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif"; // (*)
img.onload = function() {
alert(`Image loaded, size ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Error occurred while loading image");
};
但是有一些注意事项:
- 大多数资源在被添加到文档中后,便开始加载。但是
<img>要等到获得 src 后才开始加载。 <iframe>加载完成时会触发iframe.onload事件,无论是成功加载还是出现 error。
这是出于历史原因。