前言
-
问题一:前端为什么需要模块化开发
主要解决两个问题:一个是依赖问题,二是命名冲突问题。 -
问题二:AMD 和 CMD 是什么?
(1) AMD推崇依赖前置,在定义模块的时候就声明依赖的模块
(2) CMD推崇就近依赖,只有在用到某个模块的时候再去require -
问题三:AMD 和 CMD 区别?
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载时机。AMD依赖前置,js可以方便知道依赖的模块,立即加载,而CMD就近依赖,通过把模块变为字符串,解析一遍知道依赖哪些模块。所以两者都是异步加载模块。AMD在加载模块完成后就会执行该模块,所有模块加载执行完后才会执行require的回调函数,这样就会造成依赖模块的执行顺序和书写顺序不一定一致。而CMD是加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序完全一致。
正文
基于AMD和CMD等相关知识,简要分析下fis.js模块。先简单分析下fis.js结构,再分析下页面结构,最后分析下fis.js执行逻辑, 依照这样的三部曲,开始下面的旅程。
简单分析下fis.js结构
下面是fis.js源码的主体结构示意图,分为三部分:F对象、Module对象、工具函数。
(function () {
//定义全局对象F
var F = {
'version': '2.0.0',
'debug': false
}
//定义一些工具函数
//定义模块类
function Module (path, name) {
}
Module.prototype = {};
//暴露接口module: 声明一个模块
F.module = function (name, fn, deps) {};
//暴露接口use: 使用一个模块或多个模块
F.use = function(names, fn) {};
window.F = F;
})();
简单分析下页面结构
以下是以贴吧智能版为例,页面结构简要图:
<html>
<head></head>
<body>
<script src="//zhangshuai.static.tieba.otp.baidu.com/??tb/mobile/sglobal/lib/lib_1e478fb.js,tb/mobile/sglobal/lib/extend_3034d8c.js"></script>
<script src="//zhangshuai.static.tieba.otp.baidu.com/tb/mobile/sglobal/lib/common_feae225.js"></script>
<script src="//zhangshuai.static.tieba.otp.baidu.com/??/tb/mobile/app_starter_eec059c.js,/tb/mobile/aa_dff854c.js,/tb/mobile/app_open_885d443.js,/tb/mobile/ua_device_11a1c37.js,/tb/mobile/slider_6d37d9a.js,/tb/mobile/pic_free_mode_adapter_39a74e5.js,/tb/mobile/app_starter_conf_9958175.js,/tb/mobile/clouda_push_6bbb96d.js,/tb/mobile/slide_image_8c6ae49.js,/tb/mobile/slider_main_470314e.js,/tb/mobile/slider_event_aaaa509.js,/tb/mobile/slider_header_cada614.js,/tb/mobile/slider_abstract_447695c.js"></script>
<div>
....
</div>
<script>
F.use(['xxx'], function(obj){
...
});
</script>
<script>
F.use(['yyy'], function(obj){
...
});
</script>
...
</body>
</html>
第三个script 中js形如:
F.module('alert', function() {...},[]);
F.module('confirm', function(){...},[]);
F.module('dialog', function(){...},[]);
...
执行fis.js:
加载执行fis.js, 将F对象暴露给全局。
window.F = F

暴露出的API:
F.use: 指定一个或多个模块名,待模块加载完成后执行回调函数,并将模块对象依次传递给函数做参数
F.module(name,function(require, exports){},[]): 声明一个模块。模块定义函数,有两个参数,分别为require, exports。require是一个函数,用来引用其他模块;exports是一个对象,模块函数最终将模块的api挂载到exports对象上,作为模块对外输出唯一对象。
fis.js 运行流程
接下来分析下fis.js 运行流程。以贴吧智能版为例:分为两部分,第一部分是:模块的定义阶段(F.module),第二部分是:模块的调用阶段(F.use)。
模块标记对象:
Module.lazyLoadPaths = {name: true}; //F.module 时标记,在lazyLoad() 后逐一删除掉。
Module.loadedPaths = {path: true}; //已经下载完成的js文件
Module.loadingPaths = {path: true}; //正在下载的 由true -> false
Module.initingPaths = {path: true}; //初始化进行中 由true -> false
Module.requiredPaths = {name: true}; //存放依赖模块
模块定义阶段:
// 模块类:
function Module(path, name){
//模块名,在define时指定
this.name = name;
//模块js文件全路径
this.path = path;
//模块函数体
this.fn = null;
//模块对象
this.exports = {};
this._loaded = false;
//完成后需要触发的函数
this._requiredStack = [];
this._readyStack = [];
//保存实例,用于单实例判断
Module.cache[this.name] = this;
}
Module.prototype = {
...
};
//根据名称和路径获取模块实例
function get(name, path) {
if (Module.cache[name]) {
return Module.cache[name];
}
return new Module(path, name);
}
// F对象
F = {};
//模块定义接口
F.module = function(name, fn, deps){
var mod = get(name); //获取模块对象,若不存,则返回new Module(name, path);
mod.fn = fn;
mod.deps = deps || [];
if (Module.requiredPaths[name]) {
mod.define();
} else {
Module.lazyLoadPaths[name] = true;
}
}
这一阶段做的工作:
- 1、 通过get 函数,new 一个模块实例,并保存在Module.cache 中。
- 2 、在实例对象中,保存一些属性。
执行完之后:
Module.cache(存放模块实例) 保存的内容 形如:

Module.lazyLoadPaths 保存的内容 形如:

模块调用阶段:
分析两种情况: 一种是模块预加载了,另一种是模块未被预加载。
// 指定一个或多个模块名,待模块加载完成后执行回调函数,并将模块对象依次传递给函数作为参数。
F.use = function(names, fn) {
if(typeof names === 'string') {
name = [names]; //names 数组化,统一管理。name 作为Module 实例对象的唯一id。
}
forEach(names, function(name, i){
var mod = get(name); //get 函数,从Module.cache中获取相应模块,如果没有 返回一个新的new Module();
mod.ready(function(){ //第一次执行ready
args[i] = mod.exports;
if(fn){
fn.apply(null,args); //编译阶段,执行F.use 中的回调函数。
}
});
mod.lazyLoad();
}
}
function Module(path, name) {
// 模块名,在define时指定
this.name = name;
// 模块js文件全路径
this.path = path;
// 模块函数
this.fn = null;
// 模块对象
this.exports = {};
// 包括依赖是否都下载完成
this._loaded = false;
// 完成后需要触发的函数
this._requiredStack = [];
this._readyStack = [];
// 保存实例,用于单实例判断
Module.cache[this.name] = this;
}
Module.prototype = {
// 在此函数中,会调用mod中存储的fn, 也就是调用模块主体函数,抛出exports 保存在mod.exports
// 若遇到require,会执行require函数,再次调用此函数
init: function() {
if (!this._inited) {
this._inited = true;
if (!this.fn) {
throw new Error('Module "' + this.name + '" not found!');
}
var result;
Module.initingPaths[this.name] = true;
if (result = this.fn.call(null, require, this.exports)) {
this.exports = result;
}
Module.initingPaths[this.name] = false;
}
},
// 根据传入参数不同,控制流程。
ready: function(fn, isRequired) {
var stack = isRequired ? this._requireStack : this._readyStack;
if (fn) {
stack.push(fn);
}else {
this._loaded = true;
Module.loadedPaths[this.path] = true;
delete Module.loadingPaths[this.path];
this.triggerStack();
}
},
//根据模块存在的状态,执行不同的函数。
lazyLoad: function() {
var name = this.name,
path = this.path;
if (Module.lazyLoadPaths[name]) {
this.define();
delete Module.lazyLoadPaths[name];
} else {
if (Module.loadedPaths[path]) {
this.triggerStack();
} else if (!Module.loadingPaths[path]) {
Module.requiredPaths[this.name] = true;
this.load();
}
}
},
//调用分析依赖函数,若存在依赖,则执行mod.ready(fn, true), 否则执行mod.ready();
define: function() {
var _this = this,
deps = this.deps,
depPaths = [];
deps = removeCyclicDeps(_this.path, this.deps);
if (deps.length) {
Module.loadingPaths[this.path] = true;
forEach(deps, function(d) {
var mod = get(d);
depPaths.push(mod.path);
});
forEach(deps, function(d) {
var mod = get(d);
mod.ready(function() {
if (isPathsLoaded(depPaths)) {
_this.ready();
}
}, true);
mod.lazyLoad();
});
} else {
this.ready();
}
},
// 在此函数中,统一处理回调函数。先调用mod.init()
// 再调用mod.ready 传过来的回调函数,此回调函数中调用F.use中的回调函数。
triggerStack: function() {
if (this._readyStack.length > 0) {
this.init();
forEach(this._readyStack, function(func) {
if (!func.doing) {
func.doing = true;
func();
}
});
this._readyStack = [];
}
if(this._requiredStack.length > 0) {
forEach(this._readyStack, function(func){
if(!func.doing){
func.doing = true;
func();
}
});
this._requiredStack = [];
}
}
}
//实现模块的require方法
function require(name) {
var mod = get(name);
if (!Module.initingPaths[name]) { //清除循环依赖
mod.init();
}
return mod.exports;
}
优先把页面所依赖的模块,分析打包到js文件中。
- 1、names 数组化,统一处理
- 2、执行mod.ready(function(){ 执行fn()}); 第一次执行mod.ready, 将回调函数保存到了mod._readyStack
- 3、执行mod.lazyLoad(); 调用mod.define();
- 4、执行mod.define(); 由于没有依赖,所以再次执行mod.ready();
- 5、执行mod.ready(); 调用mod.triggerStack();
- 6、执行mod.triggerStack(); 先执行初始化mod.init(); 再执行mod._readyStack() 中存储的回调函数。
- 7、执行mod.init(); 执行mod.fn(); 并将 exports 保存到mod.exports中
- 8、执行mod._readyStack 中存储的函数, 在这个函数中调用F.use的回调函数。
总结
循环依赖分析:
循环依赖说明:
例如: 假设有以下三个模块A,B,C
A -> B
B -> C
C -> A
依赖关系如下 a -> [b -> [c -> [a]]]
处理c模块的依赖关系的时候,调用 removeCyclicDeps(c, [a]) 返回return [];
依赖分析源代码
function removeCyclicDeps(uri, deps) {
return filter(deps, function(dep) {
return !Module.loadingPaths[dep] || !isCyclicWaiting(Module.cache[dep], uri, []);
});
}
function isCyclicWaiting(mod, uri, track) {
if (!mod || mod._loaded)
return false;
track.push(mod.name);
var deps = mod.deps || [];
if (deps.length) {
if (indexOf(deps, uri) > -1) {
return true;
} else {
for (var i = 0; i < deps.length; i++) {
if (indexOf(track, deps[i]) < 0 && isCyclicWaiting(Module.cache[deps[i]], uri, track)) {
return true;
}
}
return false;
}
}
return false;
}
网友评论