script
标签用来在 HTML 文档中嵌入或引用可执行的脚本,当浏览器解析 HTML ,碰到 script
标签的时候,根据其不同的属性,有着不同的行为:
不带 async 或 defer 属性
<script src="script.js"></script>
没有 defer
或 async
属性时,浏览器会立即加载并执行指定的脚本。浏览器解析 HTML 文档,解析到没有 defer
或 async
属性的 script
标签时,会阻塞 HTML 文档解析,并按照它们出现的顺序执行。例如:
<script src="a.js"></script> <script> console.log('b') </script> <script src="b.js"></script>
浏览器在解析到上面的 script
标签时会阻止页面解析,并平行下载 a.js
, b.js
,依次执行 a.js
, console.log('b')
, b.js
后,再继续解析渲染后续的页面。这也是为什么你会经常看到一些建议,将需要 DOM 操作的 js 要放在 body
标签的最后,原因就是不阻塞 HTML 文档的解析,使页面尽可能快的在浏览器上渲染呈现。
带 async 属性
<script async src="script.js"></script>
带 async
的 script
标签,加载和渲染后续文档元素的过程将和脚本的加载并行进行(异步),脚本的加载完成后就马上执行,脚本执行时会阻塞 HTML 解析。
async
仅适用于外链,规定脚本异步执行- 下载不会阻塞页面解析
- 脚本的加载完成后就马上执行,脚本执行时会阻塞 HTML 解析
- 不会按照出现的顺序执行,先下载完成哪个就先执行哪个
- 执行的时候,有可能页面还没解析完成
兼容性:async attribute for external scripts
带 defer 属性
<script defer src="script.js"></script>
带 defer
的 script
标签,加载后续文档元素的过程将和脚本的加载并行进行(异步),和 async
属性不同的是,脚本的执行要在所有元素解析完成之后,DOMContentLoaded
事件触发之前完成。
defer
仅适用于外链,规定脚本延迟执行- 不会阻塞页面解析
- 在 HTML 解析完成后,
DOMContentLoaded
之前执行 - 会按照出现的顺序执行
兼容性:defer attribute for external scripts
async 和 defer 属性的异同
defer
和async
都是并行加载的,主要区别在于下载后何时执行。- 每一个
async
属性的脚本都在它下载结束之后立刻执行,所以就有可能出现脚本执行顺序被打乱的情况 - 每一个
defer
属性的脚本会在 HTML 解析完成后,DOMContentLoaded
之前,按照 DOM 中的顺序执行(ie>=10) defer
和async
都只适用于外部脚本文件,对与内联的script
标签是不起作用
type 为 module 的 script 标签
相比传统 script
,<script type="module"></script>
将被当作一个 JavaScript 模块对待,被称为 module script
,且不受 charset
和 defer
属性影响。
// app.js import { assign } from "./utils.js" var obj = Object.create( { foo: 1 }, { bar: { value: 2 }, baz: { value: 3, enumerable: true } }, ) var copy = assign({}, obj) console.log(copy)
<script type="module" src="app.js"></script> <script nomodule src="classic-app-bundle.js"></script>
上面的代码,可以这么理解:
- 支持
module script
的浏览器,不会执行拥有nomodule
属性的script
- 不支持
module script
的浏览器,会忽略未知的type="module"
的script
,同时也会忽略传统script
中不认识的nomodule
属性,进而执行传统的bundle.js
代码 module script
以及其依赖所有文件(源文件中通过import
声明导入的文件)都会被下载,一旦整个依赖的模块树都被导入,页面文档也完成解析,app.js
将会被执行- 但是如果
module script
里有async
属性,比如<script type="module" src="util.js" async></script>
,module script
及其所有依赖都会异步下载,待整个依赖的模块树都被导入时会立即执行,而此时页面有可能还没有完成解析渲染。
兼容性:JavaScript modules via script tag
图示说明
一张图片胜过千言万语,可以更好的说明传统 script
和 module script
下载,执行的情况:
理解说明:
- 使用
<script>
,脚本下载和执行会阻塞 HTML 文档的解析。 - 使用
<script defer>
,脚本下载与 HTML 解析并行,等 HTML 解析完成后没脚本都会有序执行。 - 使用
<script async>
,脚本下载与 HTML 解析并行,但一旦脚本加载完成,就会中断 HTML 解析,同时执行脚本。 <script type ="module">
的行为类似于<script defer>
,但是模块依赖关系也会被下载,<script type ="module" async>
的行为类似于<script async>
,额外的模块依赖关系也会被下载。
script 标签的 integrity 属性
<script crossorigin="anonymous" integrity="sha256-PJJrxrJLzT6CCz1jDfQXTRWOO9zmemDQbmLtSlFQluc=" src="https://assets-cdn.github.com/assets/frameworks-3c926bc6b24bcd3e820b3d630df4174d158e3bdce67a60d06e62ed4a515096e7.js"></script>
integrity
属性是资源完整性规范的一部分,它允许你为 script
提供一个 hash
,用来进行验签,检验加载的JavaScript 文件是否完整。
上面的代码来自github源码,integrity="sha256-PJJrxrJLzT6CCz1jDfQXTRWOO9zmemDQbmLtSlFQluc="
告诉浏览器,使用sha256签名算法对下载的js文件进行计算,并与intergrity提供的摘要签名对比,如果二者不一致,就不会执行这个资源。
intergrity
的作用有:
- 减少由【托管在CDN的资源被篡改】而引入的XSS 风险
- 减少通信过程资源被篡改而引入的XSS风险(同时使用https会更保险)
- 可以通过一些技术手段,不执行有脏数据的CDN资源,同时去源站下载对应资源
注意:启用 SRI 策略后,浏览器会对资源进行 CORS 校验,这就要求被请求的资源必须同域,或者配置了 Access-Control-Allow-Origin
响应头
5.script 标签的 crossorigin 属性
crossorigin
的属性值可以是 anonymous
、use-credentials
,如果没有属性值或者非法属性值,会被浏览器默认做anonymous
。
crossorigin
的作用有三个:
crossorigin
会让浏览器启用CORS访问检查,检查http相应头的Access-Control-Allow-Origin
- 对于传统
script
需要跨域获取的js资源,控制暴露出其报错的详细信息 - 对于
module script
,控制用于跨域请求的凭据模式
我们在收集错误日志的时候,通常会在window上注册一个方法来监测所有代码抛出的异常:
window.addEventListener('error', function(msg, url, lineno, colno, error) { var string = msg.toLowerCase() var substring = "script error" if (string.indexOf(substring) > -1){ alert('Script Error: See Browser Console for Detail') } else { var message = { Message: msg, URL: url, Line: lineNo, Column: columnNo, 'Error object': JSON.stringify(error) } // send error log to server record(message) } return false })
但是对于跨域js来说,只会给出很少的报错信息:'error: script error'
,通过使用 crossorigin
属性可以使跨域js暴露出跟同域js同样的报错信息。但是,资源服务器必须返回一个 Access-Control-Allow-Origin
的header,否则资源无法访问。
动态导入script(Dynamically importing scripts)
function loadError (error) { throw new URIError(`The script ${error.target.src} is not accessible.`) } function importScript (src, onLoad) { var script = document.createElement('script') script.onerror = loadError script.async = false if (onLoad) { script.onload = onLoad } document.header.appendChild(script) script.src = src }
可以上面的方法动态加载js资源,但是要注意的是,默认 append
到文档中的 script
会异步执行(可以理解为默认拥有 async
属性,如果需要加载的js按顺序执行,需要设置 async
为 false
)
script 标签的 onerror
JavaScript运行时的错误(抛出的语法错误和异常)发生时,实现了ErrorEvent接口的error事件在window上触发,并且调用window.onerror(或者window.addEventListener(‘error, cb))的回调函数
当资源(如 <img>
或 <script>
)无法加载,或者启用SRI策略资源不完整时,使用Event接口的error事件在会在该资源元素处触发,元素上的onerror回调函数被调用
script 标签与 innerHTML
通过 innerHTML
动态添加到页面上的 script
标签则不会被执行
最新评论
写的挺好的
有没有兴趣翻译 impatient js? https://exploringjs.com/impatient-js/index.html
Flexbox playground is so great!
感谢总结。
awesome!
这个好像很早就看到类似的文章了
比其他的教程好太多了
柯理化讲的好模糊…没懂