动态装载脚本是很简单的,不是通过 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 标签加载比较慢,是文件流方式加载同样数量脚本的十几倍的时间,龟速,估计是对页面装载的过程做过优化了,动态加载用得比较少就不花费精力优化了。其它浏览器也会略慢一点。