页面生命周期:DOMContentLoaded,load,beforeunload,unload
HTML 页面的生命周期包含三个重要事件:
DOMContentLoaded
—— 浏览器已完全加载 HTML,并构建了 DOM 树,但像<img>
和样式表之类的外部资源可能尚未加载完成。load
—— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。beforeunload/unload
—— 当用户正在离开页面时。 每个事件都是有用的:DOMContentLoaded
事件 —— DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。load
事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。beforeunload
事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。unload
事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。
- DOMContentLoaded
DOMContentLoaded
事件发生在document
对象上。- 我们必须使用
addEventListener
来捕获它
- DOMContentLoaded 和脚本
- 当浏览器处理一个 HTML 文档,并在文档中遇到
<script>
标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行document.write
操作,所以DOMContentLoaded
必须等待脚本执行结束。 - 不会阻塞
DOMContentLoaded
的脚本- 具有
async
特性(attribute)的脚本不会阻塞DOMContentLoaded
。 - 使用
document.createElement('script')
动态生成并添加到网页的脚本也不会阻塞DOMContentLoaded
。
- 具有
- 当浏览器处理一个 HTML 文档,并在文档中遇到
- DOMContentLoaded 和样式
- 外部样式表不会影响 DOM,因此
DOMContentLoaded
不会等待它们。 - 当
DOMContentLoaded
等待脚本时,它现在也在等待脚本前面的样式。
- 外部样式表不会影响 DOM,因此
- 浏览器内建的自动填充
- Firefox,Chrome 和 Opera 都会在
DOMContentLoaded
中自动填充表单。 - 因此,如果
DOMContentLoaded
被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。
- Firefox,Chrome 和 Opera 都会在
- window.onload
- 当整个页面,包括样式、图片和其他资源被加载完成时,会触发
window
对象上的load
事件。可以通过onload
属性获取此事件。
- 当整个页面,包括样式、图片和其他资源被加载完成时,会触发
- window.onunload
- 当访问者离开页面时,
window
对象上的unload
事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。 - 当用户要离开的时候,我们希望通过
unload
事件将数据保存到我们的服务器上。有一个特殊的navigator.sendBeacon(url, data)
方法可以满足这种需求,详见规范 https://w3c.github.io/beacon/。
- 当访问者离开页面时,
- window.onbeforeunload
- 如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,
beforeunload
处理程序将要求进行更多确认。 - 如果我们要取消事件,浏览器会询问用户是否确定。
event.preventDefault()
在beforeunload
处理程序中不起作用- 大多数浏览器都会忽略
event.preventDefault()
。
- 大多数浏览器都会忽略
- 如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,
- readyState
- 如果我们在文档加载完成之后设置
DOMContentLoaded
事件处理程序,它永远不会运行 document.readyState
属性可以为我们提供当前加载状态的信息。- 有 3 个可能值
loading
—— 文档正在被加载。interactive
—— 文档被全部读取。complete
—— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。
- 如果我们在文档加载完成之后设置
脚本:async,defer
当浏览器加载 HTML 时遇到 <script>...</script>
标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script>
也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。 这会导致两个重要的问题:
- 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
- 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在该脚本下载并执行结束前,用户都不能看到页面 一些解决办法。例如,我们可以把脚本放在页面底部。此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容
html
<body>
...all content is above the script...
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>
- defer
defer
特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。- 具有
defer
特性的脚本不会阻塞页面。 - 具有
defer
特性的脚本总是要等到 DOM 解析完毕,但在DOMContentLoaded
事件之前执行。 - 具有
defer
特性的脚本保持其相对顺序,就像常规脚本一样。 defer
特性仅适用于外部脚本- 如果
<script>
脚本没有src
,则会忽略defer
特性。
- async
async
特性与defer
有些类似。它也能够让脚本不阻塞页面。但是,在行为上二者有着重要的区别。async
特性意味着脚本是完全独立的- 浏览器不会因
async
脚本而阻塞(与defer
类似)。 - 其他脚本不会等待
async
脚本加载完成,同样,async
脚本也不会等待其他脚本。 DOMContentLoaded
和异步脚本不会彼此等待:DOMContentLoaded
可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)DOMContentLoaded
也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)
- 浏览器不会因
async
特性仅适用于外部脚本
- 动态脚本
- 可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中
JavaScriptlet script = document.createElement('script'); script.src = "/article/script-async-defer/long.js"; document.body.append(script); // (*)
- 默认情况下,动态脚本的行为是“异步”的。
- 它们不会等待任何东西,也没有什么东西会等它们。
- 先加载完成的脚本先执行(“加载优先”顺序)。
资源加载:onload,onerror
浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。
onload
—— 成功加载,onerror
—— 出现 error。
- 加载脚本
- 假设我们需要加载第三方脚本,并调用其中的函数。
JavaScriptlet script = document.createElement('script'); script.src = "my.js"; document.head.append(script);
- 对于我们自己的脚本,可以使用 JavaScript module,但是它们并未被广泛应用于第三方库。
- script.onload
load
事件会在脚本加载并执行完成时触发。
JavaScriptlet 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 ); // 显示库的版本 };
- script.onerror
- 发生在脚本加载期间的 error 会被
error
事件跟踪到。
JavaScriptlet 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
全局处理程序。
- 在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。也就是说:如果脚本成功加载,则即使脚本中有编程 error,也会触发
- 发生在脚本加载期间的 error 会被
- 其他资源
load
和error
事件也适用于其他资源,基本上(basically)适用于具有外部src
的任何资源。JavaScriptlet 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 加载完成时会触发iframe.onload
事件,无论是成功加载还是出现 error。
- 大多数资源在被添加到文档中后,便开始加载。但是
- 跨源策略
- 来自一个网站的脚本无法访问其他网站的内容。例如,位于
https://facebook.com
的脚本无法读取位于https://gmail.com
的用户邮箱。 - 更确切地说,一个源(域/端口/协议三者)无法获取另一个源(origin)的内容。因此,即使我们有一个子域,或者仅仅是另一个端口,这都是不同的源,彼此无法相互访问。
- 如果我们使用的是来自其他域的脚本,并且该脚本中存在 error,那么我们无法获取 error 的详细信息。
- 要允许跨源访问,
<script>
标签需要具有crossorigin
特性(attribute),并且远程服务器必须提供特殊的 header。 - 三个级别的跨源访问
- 无
crossorigin
特性 —— 禁止访问。 crossorigin="anonymous"
—— 如果服务器的响应带有包含*
或我们的源(origin)的 headerAccess-Control-Allow-Origin
,则允许访问。浏览器不会将授权信息和 cookie 发送到远程服务器。crossorigin="use-credentials"
—— 如果服务器发送回带有我们的源的 headerAccess-Control-Allow-Origin
和Access-Control-Allow-Credentials: true
,则允许访问。浏览器会将授权信息和 cookie 发送到远程服务器。
- 无
- 来自一个网站的脚本无法访问其他网站的内容。例如,位于
JavaScript
<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>