动态装载脚本是很简单的,不是通过 script 标签实现,就是通过 XMLHTTP 请求内容之后执行。后者会受到同域限制,而处于各种原因,脚本跨域引用是很常见的,这样对比起来前者适应性更广。

既可以用很 DOM 的方式创建 script 标签:

function getScript(path, callback) {
    var el = document.createElement("script");
    if (callback) {
        el.onload = el.onreadystatechange = function() {
            if (!this.readyState || this.readyState === 'complete' || 
                    this.readyState === 'loaded') {
                el.onload = el.onreadystatechange = null;
                callback();
            }
        };
    }
    el.src = path;
    document.getElementsByTagName("HEAD")[0].appendChild(el);
}

也可以使用 document.write():

var writeScript = function(path) {
    document.write("<script src=\""+path+"\"  type=\"text/javascript\" ><\/script>");
}

不过就不能有 callback,不知道什么时候加载结束,并且不是任何时候都能 write 的。

这个看似很入门级别的函数,用得比较少,我一直以为它会将内容写入到文档最后,跟 document.body.append 差不多的效果,当整个文档被清空之后,终于意识到这是个问题,原来一直都忽略了 document stream 的概念。

文档加载的过程可以看作是一个流,从头流到文档结束,流中的节点是按文本顺序进行渲染,所嵌入的脚本也是按顺序执行的。基于这样一个简单的理解,于是就有了常说的一项页面优化:将脚本放到文档最后不影响页面内容的渲染。而流一旦 close,再次 open 时,就会重新构造整个文档流,原来的所有东西都被清空了,而这个应该不是大多数 AJAX 应用所需要的效果。所以 document.write 是用在页面还没有渲染完毕之前动态修改文档流内容的,当页面加载完毕就不该再用了。

虽然不应该在本页面加载结束之后调用 document.write,但可以对子窗口这么做,这也是点击运行就打开一个新窗口并执行一些脚本的一种实现:

var loadInFrame = function() {
    var frame = document.createElement("iframe");
    document.body.appendChild(frame);
    var doc = frame.contentWindow.document;
    doc.open();
    doc.write("Demo here <script type=\"text/javascript\" >alert('dlrow olleh');<\/script>");
    doc.close();
}

扯完 document stream,回到动态加载脚本的问题上。

这里有个比较无聊的细节,像下面这样动态装载一个 image 是不需要 append 到 DOM tree 中就会触发装载的。

new Image().src = "path/to/image.png";

但对于 script 节点是必须插入到 DOM tree 中才会开始加载的。

通过创建 script 标签的方法加载脚本是异步执行的。因此如果有多个脚本需要加载并且是彼此存在依赖关系的,则需要依次顺序执行 getScript。

PS:实测发现 chrome 动态添加 script 标签加载比较慢,是文件流方式加载同样数量脚本的十几倍的时间,龟速,估计是对页面装载的过程做过优化了,动态加载用得比较少就不花费精力优化了。其它浏览器也会略慢一点。