动态创建 script 节点
文章比较长,真正需要的就耐心看吧。
比如我们要加载a.js,一般会这么写:
var head = document.getElementsByTagName( 'head' )[0]; var script = document.createElement( 'script' ); script.type = 'text/javascript' ; script.src = 'a.js' ; head.appendChild(script); |
说一个知识点,后面会用到:
Opera这货是个彻彻底底的两面派,比如它支持 IE 的attachEvent,也支持标准的addEventListener; 它支持IE的currentStyle,也支持标准的window.getComputedStyle;不一而足。
所以有时候专门针对IE的fix,需要排除Opera,因为它既然实现了更好的方式,我们就要用更好的,我们的目的是落后的IE,仅此而已!
Opera检测技巧:var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]';
如果我们需要调用 a.js 里的fn()方法,因为这个过程是异步的,所以要等到js文件加载完成时才能调到,怎么判断是否完成加载呢?
1 2 3 4 5 6 7 8 9 10 11 | var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]' , head = document.getElementsByTagName( 'head' )[0], script = document.createElement( 'script' ); script.type = 'text/javascript' ; if (script.attachEvent && !isOpera) { script.attachEvent( 'readystatechange' , onScriptLoad); } else { script.addEventListener( 'load' , onScriptLoad); } script.src = 'a.js' ; head.appendChild(script); |
悬念继续留给 onScriptLoad 方法,这里再插一个知识点:readyState,它包括以下值:
0: "uninitialized" – 原始状态
1: "loading" – 正在加载
2: "loaded" – 加载完成
3: "interactive" – 还未执行完毕
4: "complete" – 脚本执行完毕
为什么我写了 ":" 呢,因为在 xhr 中,请求完成时的readyState为数字形式,即 ":" 左侧的部分,而以节点加载时,readyState为字符串形式,即 ":" 右侧的部分。其中涉及的兼容性问题,请参看PPK
为什么要说这个呢,当然是为了IE 这厮。其他浏览器在脚本加载完成时,会发出 onload 事件,所以不存在问题,但是IE不知onload为何物,所以它独创一派。
经我测试Chrome14, Firefox8, Opera11, Safari5, onload 事件触发的时机是在脚本执行完之后,比如请求 a.js,这个文件的最后一行写上 alert('xxx'); 然后 script.onload = function() { alert('onload'); },打印顺序一致是 xxx -> onload。
我又试了script.addEventListener('load', function() { alert('onload'); },结果相同。
IE支持 onreadystatechange 事件,Opera 则两个都支持,同样的还是要过滤掉Opera。肯定有人会问,那IE9呢?好吧,我承认我不清楚,我只是听说IE9的addEventListener方法和别的标准浏览器表现不太一致,所以在这把IE9一并归入传统IE浏览器的范畴了。
怎么判断脚本是否加载完成呢?一般的做法是判断script.readyState,IE就是个变态,连这个值都不是固定的,所以需要这么做:
1 | script.readyState === 'loaded' || script.readyState === 'complete' |
关于这里的加载判断,我参考了好几个框架的设计,除了RequireJS 多一句 event.type === 'load',大多都是用上面这段,所以咱也用这句,要死大家一起死吧。
还有一点,因为我们统一放在 onScriptLoad 里处理,而在标准浏览器中 script.readyState 为 undefined; 为了防止内存泄漏,最好在加载完成后把 script 节点移除,参看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function onScriptLoad(e) { e = e || window.event; var script = e.target || e.srcElement; if (/loaded|complete|undefined/.test(script.readyState)) { if (script.detachEvent && !isOpera) { script.detachEvent( 'onreadystatechange' , onScriptLoad); } else { script.removeEventListener( 'load' , onScriptLoad); } var head; if (head = script.parentNode) { try { if (script.clearAttribute) { script.clearAttribute(); } else { for ( var prop in script) { delete script[prop]; } } } catch (e) { } head.removeChild(script); } } } |
再来看一个问题,现在有个需求,比如脚本 a 依赖 脚本b,a 肯定要等到 b 加载并执行完之后才能开始执行,这怎么办呢?
1. 串行加载,即一个加载完再加载下一个(较慢)
2. 并行加载
第一种方法没什么可说的,这里说第二种。
先介绍一个属性:async
当 script 的 async 属性为 true 时,脚本的执行序为异步的。即不按照加入 DOM 的顺序执行;如果是 false 则按加入的顺序执行。
如果 script 标签被直接编码到 HTML 中,黙认的 async 属性为 false;如果 script 是由 document.createElement('script') 创建的,那么 async 属性为 true。
检测方法: var script = document.createElement('script'); script.async === true;
检测结果: IE6-9, Opera11, Safari5 不支持
再介绍一个属性:defer
defer 属性规定是否延迟执行脚本,直到页面加载为止,默认值为false,具体情况可参考我最后给出的链接
触发方式:script.defer = 'defer'
检测方式:var script = document.createElement('script'); script.defer === false;
检测结果:所有浏览器都支持
相同点 | 不同点 |
---|
带有 async 或 defer 的 script 都会立刻下载,不阻塞页面解析,而且都提供一个可选的 onload 事件处理,在 script 下载完成后调用,用于做一些和此 script 相关的初始化工作。 |
script 执行的时机不同。 带有 async 的 script,一旦下载完成就开始执行(当然是在window的onload之前)。这意味着这些 script 可能不会按它们出现在页面中的顺序来执行,如果你的脚本互相依赖并和执行顺序相关,就有很大的可能出问题。 而对于带有 defer 的 script,它们会确保按在页面中出现的顺序来执行,它们执行的时机是在页面解析完后,但在 DOMContentLoaded 事件之前。 |
接着讲刚才的问题,我们关心的不是谁先加载,而是谁先执行,是执行顺序的问题,所以如果浏览器支持 async 属性,记得设置为false,然后按你需要的顺序进行appendChild就行了,但是这种方式明显是不兼容的...
还好,我们还有defer,浏览器都支持。
最后用 Script 方式 和 XHR 方式做个比较:
优点 | 缺点 |
---|
1. 具有跨域能力 2. 即使 ActiveX 被关了也可以在 IE 中运行 3. 可以在不支持 xhr 的老旧浏览器上运行 |
1. 返回的数据必须格式化为js代码,而 xhr 返回的数据可以是任何格式,XML, JSON, 纯文本等等 2. 只支持GET请求,不支持POST 3. 请求是异步还是同步完全取决于浏览器,而 xhr 可以由你控制 4. 当从一个不受信任的来源获取JSON数据时,你没办法在代码执行前检查这些数据,而 xhr 可以用一些工具分析数据,比如json2.js |
还有一个不同就是,动态创建script节点所加载的文件,如果在当前上下文调用eval()来处理,那么该文件中定义的变量和函数都是全局的。如果你希望加载的数据只是局部可用,那就用 xhr 吧。
这两种方式各有各的好处,总的来说,如果是需要加载一段代码,最好使用 动态创建script节点 的方式,如果是请求数据,最好使用 xhr。
另介绍几篇文章扩展阅读:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架