页面生命周期: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>