动态创建 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 文件加载完成时才能调到,怎么判断是否完成加载呢?

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 就是个变态,连这个值都不是固定的,所以需要这么做:

script.readyState === 'loaded' || script.readyState === 'complete'

关于这里的加载判断,我参考了好几个框架的设计,除了 RequireJS 多一句 event.type === 'load',大多都是用上面这段,所以咱也用这句,要死大家一起死吧。

还有一点,因为我们统一放在 onScriptLoad 里处理,而在标准浏览器中 script.readyState 为 undefined; 为了防止内存泄漏,最好在加载完成后把 script 节点移除,参看代码:

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 请求,不支持 POST3. 请求是异步还是同步完全取决于浏览器,而 xhr 可以由你控制 4. 当从一个不受信任的来源获取 JSON 数据时,你没办法在代码执行前检查这些数据,而 xhr 可以用一些工具分析数据,比如 json2.js

还有一个不同就是,动态创建 script 节点所加载的文件,如果在当前上下文调用 eval()来处理,那么该文件中定义的变量和函数都是全局的。如果你希望加载的数据只是局部可用,那就用 xhr 吧。

这两种方式各有各的好处,总的来说,如果是需要加载一段代码,最好使用 动态创建 script 节点 的方式,如果是请求数据,最好使用 xhr。

上次更新: 8/13/2020, 6:23:50 PM