AMD(Asynchronous Module Definition)异步模块模式
异步模块模式 : 请求发出后,继续其他业务逻辑,直到模块加载完成执行后续逻辑,实现模块开发中的对模块加载完成后的引用,大名鼎鼎的require.js就是以它为思想的。
这里导出了一个问题,就是script标签是异步加载还是同步加载的 ?
按照经验:script是顺序加载的,例如引用jquery的脚本需要放在src=jquery的脚本之后。
<script src="jQuery.js"/>
<script src="a.js"/>
<script src="b.js"/>
<script src="c.js"/>
这样的引用会有以下缺点:
- 必须按顺序引入
- 同步加载各个js,只有1.js加载并执行完才去加载2.js
- 各个js文件可能会有多个window全局变量的创建、污染
题外 : async 与 defer 有什么区别?
- defer : 阻塞加载,渲染后再执行(DOM结构完全成和前面脚本执行完全后执行,script默认为true)
- async : h5新增属性,异步加载,下载完后立即执行
对同步模式(SMD)的思考
在服务器端中,本质上是使用一个脚本在服务器主机上跑,它使用编译-运行的这种模式,就算它是一次加载所有脚本文件,导致的结果最多是编辑脚本时间变长。
但是浏览器端能用这种模式吗? 答: 在网页功能复杂的项目,势必不能使用这种方法!因为浏览器中对文件的加载是阻塞加载的,如果js文件庞大,js对视图层嵌入层度深,势必会影响用户的首屏加载速度,用户体验会非常糟糕。
所以,有的开发者就像,如果我在同步模块的基础上改造,引用其他模块时,动态创建一个script标签进行加载,加载完成后才执行回调函数。
var loadscript = function(src) {
var _script = document.createElemenet('script')
_script.type = 'text/Javascript'
_script.src = src
document.getElementsByTagName('head')[0].appendChild(_script)
}
loadscript('b.js')
F.module('b',function(b) {
// do something
})
以上代码有没有问题 ? 有!
script插入的时候,浏览器会异步加载这个文件,但没等它加载完成,F.module就执行了。
AMD的简单实现
看完张容铭的异步模块代码,写了一个异步模块模式,这种形式类似promise的实现,先来看看代码
var F = (function () {
var moduleCache = {}
// 定义模块
var module = function (url, modDeps, modCallback) {
var args = [].slice.call(arguments),
deps, // 依赖模块列表
callback, // 回调函数
mods = [], // 依赖模块集合
i = 0, // 依赖模块序列中的索引
len, // 依赖模块数组长度
depsCount = 0 // 未加载的依赖摸摸看数量统计
// 多态
if (args.length > 2) {
deps = modDeps instanceof Array ? modDeps : [modDeps]
callback = modCallback
} else {
deps = []
callback = modDeps
}
len = deps.length
if (len) {
while (i < len) {
depsCount++;
(function (i) {
loadModule(deps[i], function (mod) {
mods.push(mod)
depsCount--
if (depsCount === 0) {
setModule(url, mods, callback)
}
})
})(i)
i++
}
} else {
setModule(url, [], callback)
}
}
// 注册模块 并执行模块构造函数
var setModule = function (moduleName, mods, callback) {
var _module
if (moduleCache[moduleName]) {
// 获取模块
_module = moduleCache[moduleName]
_module.status = 'loaded'
_module.exports = callback && callback.apply(_module, mods)
while (_module.onload.length > 0) {
var _fn = _module.onload.shift()
_fn(_module.exports)
}
} else {
callback && callback.apply(null, mods)
}
}
// 加载模块
var loadModule = function (moduleName, callback) {
var _module
if (moduleCache[moduleName]) {
_module = moduleCache[moduleName]
if (_module.status === 'loaded') {
setTimeout(callback(_module.exports), 0)
} else {
_module.onload.push(callback)
}
} else {
// 模块第一次调用
moduleCache[moduleName] = {
moduleName,
status: 'loading',
exports: null,
onload: [callback]
};
// 加载模块对应文件
loadScript('./' + moduleName + '.js')
}
}
var loadScript = function (src) {
var _script = document.createElement('script')
_script.type = 'text/Javascript'
_script.src = src
_script.async = true
document.getElementsByTagName('head')[0].appendChild(_script)
}
return {
module
}
}())
F.module('lib/main', ['lib/a', 'lib/b'], function (a, b) {
console.log(a)
console.log(b)
})
在index.html同级目录创建lib文件夹,在其中创建a.js、b.js两个文件,代码为
F.module('lib/a', function () {
return {
name: ' a is 2233 '
}
})
F.module('lib/b', function () {
return {
name: ' b is 33333 '
}
})
流程如下 : 可能还是不是很清楚
1. 代码分析
F.module('lib/main', ['lib/a', 'lib/b'], function (a, b) {
console.log(a)
console.log(b)
})
在这段代码中,它需要两个依赖模块lib/a,lib/b
然后module方法执行。
关键是这段代码,他给未加载的模块创建了一个回调函数,并且用闭包记录了未加载模块数的值与cb使用的依赖
(function (i) {
loadModule(deps[i], function (mod) {
mods.push(mod)
depsCount--
if (depsCount === 0) {
setModule(url, mods, callback)
}
})(i)
接着看loadModule,如果模块未加载则在cache中创建它的实例,并且设置状态为loading中,嵌入上述的回调函数
moduleCache[moduleName] = {
moduleName,
status: 'loading',
exports: null,
onload: [callback]
};
// 加载模块对应文件
loadScript('./' + moduleName + '.js')
当a.js文件加载完成后,则会调用
F.module('lib/a', function () {
return {
name: ' a is 2233 '
}
})
在module方法中,如果它没有依赖相关模块,则会直接在cache中注册,执行setModule
setModule(url, [], callback)
接着,他发现cache中竟然有自己的实例(被依赖了)
if (moduleCache[moduleName]) {
// 获取模块
_module = moduleCache[moduleName]
_module.status = 'loaded'
_module.exports = callback && callback.apply(_module, mods)
while (_module.onload.length > 0) {
var _fn = _module.onload.shift()
_fn(_module.exports)
}
}
上述代码会做如下事情
- 将自身状态status改为loaded
- 将自己的回调函数返回结果给cache实例,赋值为exports
- 执行onload函数
最后,知道最后一个模块加载完毕,onload执行,发现depCount数已经为0了,则会执行lib/main
的回调方法.