html文件中的<script>标签中的代码或src引用的js文件中的代码是同步加载和执行的
html文件中的<script>标签中的代码使用document.write()方式引入的js文件是异步执行的
Web动态加载JS外部文件(script标签)
异步加载文件。当页面还有同步代码执行的时候。异步加载从控制台上看到的显示。一直是pending。文件很小的额话其实早就加载回来了。只是js是单线程的。没有功夫去处理显示状态。
面向对象的方式实现require.js
问题:这里都有哪些类型的对象呢?
答案:至少有模块(Module)这一类对象
那模块类对象有哪些数据呢?
Module.id // 模块id
Module.name // 模块名字
Module.src // 模块的真实的uri路径
Module.dep // 模块的依赖
Module.cb // 模块的成功回调函数
Module.errorFn // 模块的失败回调函数
Module.STATUS // 模块的状态(等待中、正在网络请求、准备执行、执行成功、出现错误……)
又有哪些对应的操作这些数据的方法呢?
Module.prototype.init // 初始化,用来赋予各种基本值
Module.prototype.fetch // 通过网络请求获取模块
Module.prototype.analyzeDep // 分析、处理模块的依赖
Module.prototype.execute // 运算该模块
先想一下require.js是怎么使用的。
index.html页面引入
<script type="text/javascript" src="./require.js" data-main="main"></script>
data-main 是主文件的入口。main.js里就是主文件,里面有require。然后其他js文件就是定义模块,define()。
假设main.js现在如下
require(['a', 'b'], function (a, b) {
a.hi();
b.goodbye();
}, function () {
console.error('Something wrong with the dependent modules.');
});
这个文件执行的时候。我们先要去加载依赖的a和b模块。我们既然用Module对象来描述每一个模块对象。一般构造函数都如下。有个init
function Module(name, dep, cb, errorFn) {
this.init(name, dep, cb, errorFn);
}
所以我们在解析main.js时候就分别对a和b实例化。只是这时候的实例化的主要目的是执行Module上的加载js函数。
如何异步加载js文件呢。这里用的是动态生成script标签。新的<script>元素加载js文件。此文件当元素添加到页面之后立刻开始下载。等到没有同步代码执行时候。立刻执行加载好的js文件。
当执行加载好的a.js文件时候,就会执行define函数。这里我们队刚刚实例化的a模块进行丰富。
let module = modules[name];
module.name = name;
module.dep = dep;
module.cb = cb;
module.errorFn = errorFn;
如果define
函数还依赖别的模块。要继续去加载别的模块。
碰到了一个难点:如何分析和处理模块的依赖?
举个例子:main.js
必须等a和b模块都加载执行完成后才能执行main的回调。
我想了一个方法:记数法。分两步走。
- 为
Module
原型新增Module.depCount
属性,初始值为该模块依赖模块数组的长度。 - 假如
depCount===0
,说明该模块依赖的模块都已经运算好了,通过setter触发执行该模块。 - 某模块执行成功之后,触发下一步。
- 下一步为:通过对象
mapDepToModuleOrTask
,查找到依赖与该模块的所有模块,那么让那些模块都执行depCount--
。
注:对象mapDepToModuleOrTask
的作用是映射被依赖模块到依赖模块之间的关系。
结构如下图所示。举个例子:当模块a准备好之后,我们就遍历mapDepToModule['a']对应的数组,里面的每一项都执行depCount--。
分析依赖模块这个方法
Module.prototype.analyzeDep = function () {
let depCount = this.dep ? this.dep.length : 0;// 依赖的模块数
if (depCount === 0) {//如果不依赖别的模块,直接执行回调。
this.execute();//执行模块回调
return;
}
Object.defineProperty(this, 'depCount', { // 如果依赖别的模块,就增加一个depCount的属性。当依赖加载完一个depCount就--。知道depCount=0。触发回调函数
get() {
return depCount;
},
set(newDepCount) {
depCount = newDepCount;
if (newDepCount === 0) {
this.execute();
}
}
});
this.dep.forEach((depModuleName) => { // 遍历该模块的依赖模块,再加载依赖模块
if (!modules[depModuleName]) { // 映射所有依赖该(depModuleName)模块的模块
let module = new Module(depModuleName);
modules[depModuleName] = module;
}
if (!mapDepToModuleOrTask[depModuleName]) {
mapDepToModuleOrTask[depModuleName] = [];
}
mapDepToModuleOrTask[depModuleName].push(this); //当前模块的依赖模块都push当前模块
});
}
处理依赖循环
我们有时候会定于循环依赖的模块,比如a需要b并且b需要a,会造成死循环(可以在代码中判断是循环依赖的话a需要b,b有需要a。在加载b模块de时候。不再去加载a模块)。这样就不会造成死循环了,但是在这个情况下当b模块调用时他将会从a获得一个undefined值。所以解决办法是模块b的回调函数中,并不能直接引用到a,需要使用require方法包住。
处理办法
// a.js
define(['b'],function (b) {
var hi = function () {
console.log('hi');
};
b.goodbye();
return {
hi: hi
}
});
// b.js
define(['require', 'a'], function (require) {
var goodbye = function () {
console.log('goodbye');
};
// 因为在运算b的时候,a还没准备好,所以不能直接拿到a,只能用require再发起一次新的任务
require(['a'], function (a) {
a.hi();
});
return {
goodbye: goodbye
}
});
这样一来原先的require.js
就有问题了
原先的设计中, 每一个define
是跟一个模块一一对应的, require
只能用一次,用于主入口模块(如:main.js)的加载。现在define中还需要解析require
,require也需要解析依赖,执行回调。所以require也应当是一个模块。但这个模块不需要fetch。我将它命名为:任务(Task),这是一个有别于Module
的新的类。
每一次调用require,相当于新建一个Task(任务)。这个任务的功能是:当任务的所有依赖都准备好之后,执行该任务的成功回调函数。
有没有发现这个Task
原型与Module
很像?它们都有依赖、回调、状态,都需要分析依赖、执行回调函数等方法。但是又有些不同,比如Task
没有网络请求,所以不需要fetch
这样的方法。
所以,我让Task继承了Module,然后重写某些方法。
作者的博客--实现require
作者的代码
我仿照写的代码
JavaScript 模块简史