# how-to-load-dynamic-script
**Repository Path**: mirrors_singod/how-to-load-dynamic-script
## Basic Information
- **Project Name**: how-to-load-dynamic-script
- **Description**: The right way to load dynamic script
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-09-25
- **Last Updated**: 2026-03-08
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
动态加载js文件的正确姿势
---
**说明:**
这个repository的结构:
* `img/`:图片
* `LABjs-source/`:[LABjs](https://github.com/getify/LABjs)的源码,带注释,文中部分代码参考了该项目。
* `lazyload-source`:[lazyload](https://github.com/rgrove/lazyload)的源码,带注释,文中部分代码参考了该项目。
* `src/`:本文档中涉及的代码,**在Firefox 42中测试,使用Firebug观察和调试**。
* `README.md`:本文档。
本文中给出了多种解决方式,`方式1`对应的代码是`src/js/loader01.js`和`src/index01.js`,其他方式对应的代码位置类似。
> Gif图片使用[LICEcap](http://www.cockos.com/licecap/)生成。
---
最近在做一个为网页生成目录的工具[awesome-toc](https://github.com/someus/awesome-toc),该工具提供了以jquery插件的形式使用的代码,也提供了一个基于[Bookmarklet](https://en.wikipedia.org/wiki/Bookmarklet)(小书签)的浏览器插件。
小书签需要向网页中注入多个js文件,也就相当于动态加载js文件。在编写这部分代码时候遇到坑了,于是深究了一段时间。
我在这里整理了动态加载js文件的若干思路,**这对于理解异步编程很有用处,而且也适用于Nodejs**。
## 硬编码在html源码中的script是如何加载的
如果html中有:
```html
```
那么,浏览器解析到
```html
```
会停止渲染页面,去拉取`1.js`(IO操作),等到`1.js`的内容获取到后执行。
1.js执行完毕后,浏览器解析到
```html
```
进行和`1.js`类似的操作。
不过现在部分浏览器支持async属性和defer属性,这个可以参考:
[async vs defer attributes](http://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html)
[script的defer和async](http://ued.ctrip.com/blog/script-defer-and-async.html)
[script -MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)指出:async对内联脚本(inline script)没有影响,defer的话因浏览器以及版本不同而影响不同。
## 从一个例子出发
举个实际的例子:
```html
```
js/your.js:
```js
console.log('your.js: time='+Date.parse(new Date()));
function myAlert(msg) {
console.log('alert at ' + Date.parse(new Date()));
alert(msg);
}
function myLog(msg) {
console.log(msg);
}
```
js/my.js:
```js
myLog('my.js: time='+Date.parse(new Date()));
$('#only-button').click(function() {
myAlert("hello world");
});
```
可以看出`jquery`、`js/your.js`、`js/my.js`三者的关系如下:
* `js/my.js`依赖于`jquery`和`js/your.js`。
* `jquery`和`js/your.js`之间没有依赖关系。
浏览器打开`index00.html`,等待js加载完毕,点击按钮`hello world`将会触发`alert("hello world");`。
firbug控制台输出:

下面开始探索如何动态加载js文件。
## 方式1:一个错误的加载方式
文件js/loader01.js内容如下:
```js
Loader = (function() {
var loadScript = function(url) {
var script = document.createElement( 'script' );
script.setAttribute( 'src', url+'?'+'time='+Date.parse(new Date())); // 不用缓存
document.body.appendChild( script );
};
var loadMultiScript = function(url_array) {
for (var idx=0; idx < url_array.length; idx++) {
loadScript(url_array[idx]);
}
}
return {
load: loadMultiScript,
};
})(); // end Loader
```
index01.html内容如下:
```html
```
浏览器打开`index01.html`,点击按钮`hello world`,会发现什么都没发生。打开firebug,进入控制台,可以看到这样的错误:

很明显,`my.js`没等jquery就先执行了。又由于存在依赖关系,脚本的执行出现了错误。这不是我想要的。
在网上可以找到关于动态加载的一些说明,例如:
> Opera/Firefox(老版本)下:脚本执行的顺序与节点被插入页面的顺序一致
>
> IE/Safari/Chrome下:执行顺序无法得到保证
>
> 注意:
>
> 新版本的Firefox下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行
> 老版本的Chrome下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行
真够乱的!!(这段描述来自:[LABJS源码浅析](http://www.cnblogs.com/chyingp/archive/2012/10/17/2726898.html)。)
为了解决我们遇到的问题,我们可以在loadScript函数中修改script对象async的值:
```js
var loadScript = function(url) {
var script = document.createElement('script');
script.async = false; // 这里
script.setAttribute('src', url+'?'+'time='+Date.parse(new Date()));
document.body.appendChild(script);
};
```
浏览器打开,发现可以正常执行!可惜该方法只在某些浏览器的某些版本中有效,没有通用性。[script browser compatibility](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Browser_compatibility)给出了下面的兼容性列表:

下面探索的方法都可以正确的加载和执行多个脚本,不过有些同样有兼容性问题(例如Pormise方式)。
## 方式2
可以认为绝大部分浏览器动态加载脚本的方式如下:
1. 动态加载多个脚本时,这些脚本的加载(IO操作)可能并行,可能串行。
2. 一个脚本一旦加载完毕(IO结束),该脚本放入“待执行队列”,等待出队供js引擎去执行。
所以我们的示例中的三个js脚本的加载和执行顺序可以是下面的情况之一:
1. `jquery`加载并执行,`js/your.js`加载并执行,`js/my.js`加载并执行。
2. 和情况1类似,不过`js/your.js`在前,`jquery`在后。
3. `jquery`和`js/your.js`并行加载,按照加载完毕的顺序来执行;等`jquery`和`js/your.js`都执行完毕后,加载并执行`js/my.js`。
其中,“加载完毕”这是一个事件,浏览器的支持监测这个事件。这个事件在IE下是`onreadystatechange `,其他浏览器下是`onload `。
据此,[Loading JavaScript without blocking](https://www.nczonline.net/blog/2009/06/23/loading-javascript-without-blocking/)给出了下面的代码:
```js
function loadScript(url, callback){
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function(){
callback();
};
}
script.src = url;
document.body.appendChild(script);
}
```
callback函数可以是去加载另外一个js,不过如果要加载的js文件较多,就成了“回调地狱”(callback hell)。
回调地狱式可以通过一些模式来解决,例如下面给出的方式2:
```js
Loader = (function() {
var load_cursor = 0;
var load_queue;
var loadFinished = function() {
load_cursor ++;
if (load_cursor < load_queue.length) {
loadScript();
}
}
function loadError (oError) {
console.error("The script " + oError.target.src + " is not accessible.");
}
var loadScript = function() {
var url = load_queue[load_cursor];
var script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
loadFinished();
}
};
} else { //Others
script.onload = function(){
loadFinished();
};
}
script.onerror = loadError;
script.src = url+'?'+'time='+Date.parse(new Date());
document.body.appendChild(script);
};
var loadMultiScript = function(url_array) {
load_cursor = 0;
load_queue = url_array;
loadScript();
}
return {
load: loadMultiScript,
};
})(); // end Loader
//loading ...
Loader.load([
'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'./js/your.js',
'./js/my.js'
]);
```
`load_queue`是一个队列,保存需要依次加载的js的url。当一个js加载完毕后,`load_cursor++`用来模拟出队操作,然后加载下一个脚本。
onerror事件也添加了回调,用来处理无法加载的js文件。当遇到无法加载的js文件时停止加载,剩下的文件也不会加载了。
效果如下:

## 方式3
方式2是串行的去加载,我们稍加改进,让可以并行加载的js脚本尽可能地并行加载。
```js
Loader = (function() {
var group_queue; // group list
var group_cursor = 0; // current group cursor
var current_group_finished = 0;
var loadFinished = function() {
current_group_finished ++;
if (current_group_finished == group_queue[group_cursor].length) {
next_group();
loadGroup();
}
};
var next_group = function() {
current_group_finished = 0;
group_cursor ++;
};
var loadError = function(oError) {
console.error("The script " + oError.target.src + " is not accessible.");
};
var loadScript = function(url) {
console.log("load "+url);
var script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function() {
if (script.readyState == "loaded" ||
script.readyState == "complete") {
script.onreadystatechange = null;
loadFinished();
}
};
} else { //Others
script.onload = function(){
loadFinished();
};
}
script.onerror = loadError;
script.src = url+'?'+'time='+Date.parse(new Date());
document.body.appendChild(script);
};
var loadGroup = function() {
if (group_cursor >= group_queue.length)
return;
current_group_finished = 0;
for (var idx=0; idx < group_queue[group_cursor].length; idx++) {
loadScript(group_queue[group_cursor][idx]);
}
};
var loadMultiGroup = function(url_groups) {
group_cursor = 0;
group_queue = url_groups;
loadGroup();
}
return {
load: loadMultiGroup,
};
})(); // end Loader
//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
your = './js/your.js',
my = './js/my.js'
;
Loader.load([ [jquery, your], [my] ]);
```
`Loader.load([ [jquery, your], [my] ]);`代表着`jquery`和`js/your.js`先尽可能快地加载和执行,等它们执行结束后,加载并执行`./js/my.js`。
这里将每个子数组里的所有url看成一个group,group内部的脚本尽可能并行加载并执行,group之间则为串行。
这段代码里使用了一个计数器`current_group_finished`记录当前group中完成的url的数量,在这个数量和url的总数一致时,进入下一个group。
效果如下:

## 方式4
该方式是对方式3中代码的重构。
```js
Loader = (function() {
var group_queue = []; // group list
var current_group_finished = 0;
var finish_callback;
var finish_context;
var loadFinished = function() {
current_group_finished ++;
if (current_group_finished == group_queue[0].length) {
next_group();
loadGroup();
}
};
var next_group = function() {
group_queue.shift();
};
var loadError = function(oError) {
console.error("The script " + oError.target.src + " is not accessible.");
};
var loadScript = function(url) {
console.log("load "+url);
var script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function() {
if (script.readyState == "loaded" ||
script.readyState == "complete") {
script.onreadystatechange = null;
loadFinished();
}
};
} else { //Others
script.onload = function(){
loadFinished();
};
}
script.onerror = loadError;
script.src = url+'?'+'time='+Date.parse(new Date());
document.body.appendChild(script);
};
var loadGroup = function() {
if (group_queue.length == 0) {
finish_callback.call(finish_context);
return;
}
current_group_finished = 0;
for (var idx=0; idx < group_queue[0].length; idx++) {
loadScript(group_queue[0][idx]);
}
};
var addGroup = function(url_array) {
if (url_array.length > 0) {
group_queue.push(url_array);
}
};
var fire = function(callback, context) {
finish_callback = callback || function() {};
finish_context = context || {};
loadGroup();
};
var instanceAPI = {
load : function() {
addGroup([].slice.call(arguments));
return instanceAPI;
},
done : fire,
};
return instanceAPI;
})(); // end Loader
//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
your = './js/your.js',
my = './js/my.js'
;
// Loader.load(jquery, your).load(my).done();
Loader.load(jquery, your)
.load(my)
.done(function(){console.log(this.msg)}, {msg: 'finished'});
```
在调用多次load()函数后,必须调用done()函数。done()函数用来触发所有脚本的load。
## 方式5
这个方式是对方式4的重写。改进为调用load()时候尽可能去触发实际的load操作。
```js
// 这里调试用的代码我没有删除
Loader = (function() {
var group_queue = []; // group list
//// url_item = {url:str, start: false, finished:false}
// 用于调试
var log = function(msg) {
return;
console.log(msg);
}
var isFunc = function(obj) {
return Object.prototype.toString.call(obj) == "[object Function]";
}
var isArray = function(obj) {
return Object.prototype.toString.call(obj) == "[object Array]";
}
var isAllStart = function(url_items) {
for (var idx=0; idx 0) {
showGroupInfo();
// is Func
if (isFunc(group_queue[0])) {
log('## nextGroup: exec func');
group_queue[0](); // exec
group_queue.shift();
continue;
// is Array
} else if (isAllFinished(group_queue[0])) {
log('## current group all finished');
group_queue.shift();
continue;
} else if (!isAnyStart(group_queue[0])) {
log('## current group no one start!');
loadGroup();
break;
} else {
break;
}
}
};
var loadError = function(oError) {
console.error("The script " + oError.target.src + " is not accessible.");
};
var loadScript = function(url_item) {
log("load "+url_item.url);
url = url_item.url;
url_item.start = true;
var script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function() {
if (script.readyState == "loaded" ||
script.readyState == "complete") {
script.onreadystatechange = null;
url_item.finished = true;
loadFinished();
}
};
} else { //Others
script.onload = function(){
url_item.finished = true;
loadFinished();
};
}
script.onerror = loadError;
script.src = url+'?'+'time='+Date.parse(new Date());
document.body.appendChild(script);
};
var loadGroup = function() {
for (var idx=0; idx < group_queue[0].length; idx++) {
loadScript(group_queue[0][idx]);
}
};
var addGroup = function(url_array) {
log('add :' + url_array);
if (url_array.length > 0) {
group = [];
for (var idx=0; idx