document.createElement('script') 和 DOMContentLoaded 执行顺序
上篇在翻译一篇文章时看到:
脚本不阻塞DOMContentLoaded
此规则有两个特例:
- 脚本有 async 属性,我们稍后会提到此属性,不会阻塞 DOMContentLoaded
- 脚本由 document.createElement('script') 动态生成,然后加入到html文档,也不会阻塞 DOMContentLoaded
上边提到的第一条规则我之前是知道的: async 脚本会在 load 事件之前发生,但不能保证 DOMContentLoaded 事件先后的执行顺序
,也就是说不一定阻塞渲染(如果文件过大,下载时间长(浏览器渲染和js文件下载并行执行),有可能在浏览器渲染完成后才下载完,也就是在DOMContentLoaded事件触发后才执行)
那么,第二条规则的我之前没有留意过,有可能跟平时使用的少有关(几乎没有使用过)。我第一时间会质疑:这个说法成立吗?
实践出真知,实例代码如下:
loadScripts([
'./dynamic-script.js',
], function () {
alert('dynamic-script: loaded')
})
document.addEventListener('DOMContentLoaded', () => {
alert('DOMContentLoaded')
})
window.onload = () => {
alert('onload')
}
function loadScripts(array,callback){
var loader = function(src,handler){
var script = document.createElement("script");
script.src = src;
script.onload = script.onreadystatechange = function(){
script.onreadystatechange = script.onload = null;
handler();
console.log(script.async) // 注1
}
var head = document.getElementsByTagName("head")[0];
(head || document.body).appendChild( script );
};
(function run(){
if(array.length!=0){
loader(array.shift(), run);
}else{
callback && callback();
}
})();
}
// dynamic-script.js
alert('dynamic-script-content')
执行结果如下:
- 打印 'DOMContentLoaded'
- 打印 'dynamic-script-content'
- 打印 'dynamic-script: loaded'
- 打印 true(控制台)
- 打印 'onload'
结果跟上边的描述是一致的。。。
网上查阅资料解释:JavaScript异步加载的三种方式——async和defer、动态创建script
在没有定义defer和async之前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中。
MDN解释: <script>
[1] 在不支持该async属性的旧浏览器中,解析器插入的脚本会阻塞解析器;插入脚本的脚本在 IE 和 WebKit 中异步执行,但在 Opera 和 4.0 之前的 Firefox 中同步执行。在 Firefox 4.0 中,asyncDOM 属性默认为true用于脚本创建的脚本,因此默认行为与 IE 和 WebKit 的行为相匹配。
要请求在document.createElement("script").async评估为的浏览器true(例如 Firefox 4.0)中以插入顺序执行插入脚本的外部脚本
,请.async=false在要维护顺序的脚本上设置。永远不要document.write()从async脚本中调用。在 Gecko 1.9.2 中,调用document.write()具有不可预测的效果。在 Gecko 2.0 中,document.write()从async 脚本无效(除了向错误控制台打印警告)。
此外,还有一篇文章提到: DOMContentLoaded talk about blocking and rendering - analysis and resource loading html page events
为了验证上边的解释,我在实例代码中(// 注1)处对script的async值进行了输出,由 document.createElement("script")
创建的脚本确实是异步的。
记忆技巧:
首先我们来看 document.createElement("script")
,这里边有个 document
... 也就是说我们需要使用 document
对象,何时才能访问到 document
对象? 答案显而易见:dom树构建完毕时(此时页面还没有生成cssom,render tree,布局,绘制balabala)。。。我们考虑下这段动态插入的脚本肯定不是很重要的(如果很重要,需要在关键渲染路径之前执行,肯定会写为内联脚本),既然这段脚本不是很重要,那就不能影响关键渲染路径的性能,那就等浏览器首屏渲染完成后(这句可能不那么准确)再下载执行也不迟。。。所以设计者就将其放在 DOMContentLoaded
之后下载执行,从而实现异步加载,也就是不阻塞 DOMContentLoaded
事件。