做前端项目已经离不开SeaJS了,现在的很多的网站都用了SeaJS,对这个JS模块加载器原理的越来越感兴趣。这篇文章我们来一起学习seajs模块之间依赖的加载以及模块的执行,感兴趣的朋友们下面来一起看看吧。
本文介绍的是seajs模块之间依赖的加载以及模块的执行,下面话不多说直接来看详细的介绍。
入口方法
每个程序都有个入口方法,类似于c的main 函数,seajs也不例外。系列一的demo在首页使用了seajs.use() ,这便是入口方法。入口方法可以接受2个参数,第一个参数为模块名称,第二个为回调函数。入口方法定义了一个新的模块,这个新定义的模块依赖入参提供的模块。然后设置新模块的回调函数,用以在loaded 状态之后调用。该回调函数主要是执行所有依赖模块的工厂函数,最后在执行入口方法提供的回调。
// Public API
// 入口地址
seajs.use = function(ids, callback) {
Module.preload(function() {
Module.use(ids, callback, data.cwd + "_use_" + cid())
})
return seajs
}
// Load preload modules before all other modules
Module.preload = function(callback) {
var preloadMods = data.preload
var len = preloadMods.length
if (len) {
Module.use(preloadMods, function() {
// Remove the loaded preload modules
preloadMods.splice(0, len)
// Allow preload modules to add new preload modules
Module.preload(callback)
}, data.cwd + "_preload_" + cid())
}
else {
callback()
}
}
// Use function is equal to load a anonymous module
Module.use = function (ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
mod.callback = function() {
var exports = []
var uris = mod.resolve()
for (var i = 0, len = uris.length; i < len; i++) {
exports = cachedMods[uris].exec()
}
// 回调函数的入参对应依赖模块的返回值
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
}
mod.load()
}
Module.preload 用于预加载seajs提供的插件plugins,非主要功能,可以忽略。而Module.use 则是核心方法,该方法正如之前所说,创建新的module并设置回调函数,最后加载新模块的所有依赖模块。
加载依赖之load方法
load 方法可谓是seajs的精华所在。该方法主要加载依赖模块并依次执行依赖模块的回调函数,最终执行的回调函数则是通过seajs.use(“./name”) 创建的新模块的回调,也就是mod.callback 。
load 方法递归加载依赖模块,如果依赖模块还依赖其他模块,则再加载这个模块。这是通过Module 类中的_waitings 和_remain 来实现的。
Module.prototype.load = function() {
var mod = this
// If the module is being loaded, just wait it onload call
if (mod.status >= STATUS.LOADING) {
return
}
mod.status = STATUS.LOADING
// Emit `load` event for plugins such as combo plugin
var uris = mod.resolve()
emit("load", uris, mod)
var len = mod._remain = uris.length
var m
// Initialize modules and register waitings
for (var i = 0; i < len; i++) {
m = Module.get(uris)
// 修改 依赖文件 的 _waiting属性
if (m.status < STATUS.LOADED) {
// Maybe duplicate: When module has dupliate dependency, it should be it's count, not 1
m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1
}
else {
mod._remain--
}
}
// 加载完依赖,执行模块
if (mod._remain === 0) {
mod.onload()
return
}
// Begin parallel loading
var requestCache = {}
for (i = 0; i < len; i++) {
m = cachedMods[uris]
// 该依赖并未加载,则先fetch,将seajs.request函数绑定在对应的requestCache上,此时并未加载模块
if (m.status < STATUS.FETCHING) {
m.fetch(requestCache)
}
else if (m.status === STATUS.SAVED) {
m.load()
}
}
// Send all requests at last to avoid cache bug in IE6-9. Issues#808
// 加载所有模块
for (var requestUri in requestCache) {
if (requestCache.hasOwnProperty(requestUri)) {
// 此时加载模块
requestCache[requestUri]()
}
}
}
// 依赖模块加载完毕执行回调函数
// 并检查依赖该模块的其他模块是否可以执行
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
if (mod.callback) {
mod.callback()
}
console.log(mod)
// Notify waiting modules to fire onload
var waitings = mod._waitings
var uri, m
for (uri in waitings) {
if (waitings.hasOwnProperty(uri)) {
m = cachedMods[uri]
m._remain -= waitings[uri]
if (m._remain === 0) {
m.onload()
}
}
}
// Reduce memory taken
delete mod._waitings
delete mod._remain
}
首先初始化模块的_waitings 和_remain 属性,如果_remain 为0,则意味着没有依赖或者依赖已加载,可以执行onload 函数;如果不为0,则fetch 未加载的模块。在这里有个实现的小技巧,就是同时加载所有依赖:requestCache 对象保存加载函数:(在fetch 函数中定义)
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
其中,sendRequest 函数定义如下:
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
并行加载所有依赖,当依赖加载完毕,执行onRequest 回调,向上冒泡,加载依赖的依赖,直至没有依赖模块。
当最上层的依赖已没有依赖模块时,执行onload 函数,在函数体内设置状态为loaded ,执行mod.callback, 并检查并设置该模块的_waitings 属性,判断下层模块是否还有依赖,若没有则执行下层模块的mod.callback ,这一依次回溯,最终将会执行通过seajs.use 创建的匿名模块的mod.callback 。
例证
通过一个简单的例子,论证上述过程:
tst.html
<script>
seajs.use('./b');
</script>
-------------------------------------
a.js
define(function(require,exports,module){
exports.add = function(a,b){
return a+b;
}
})
------------------------------------
b.js
define(function(require,exports,module){
var a = require("./a");
console.log(a.add(3,5));
})
通过调试工具,可以看出执行onload 的次序:
最后可看出,匿名模块的状态码为4,也就是该模块并未执行.确实,也没有给匿名模块定义工厂函数,无法执行.
模块执行之exec
模块执行是在seajs.use 中定义的mod.callback 中调用的,依次调用所有依赖的exec 方法,执行程序逻辑。exec 方法中有commonJS的一些重要关键字或者函数,如require ,exports 等,让我们一看究竟:
Module.prototype.exec = function () {
var mod = this
// When module is executed, DO NOT execute it again. When module
// is being executed, just return `module.exports` too, for avoiding
// circularly calling
if (mod.status >= STATUS.EXECUTING) {
return mod.exports
}
mod.status = STATUS.EXECUTING
// Create require
var uri = mod.uri
function require(id) {
return Module.get(require.resolve(id)).exec()
}
require.resolve = function(id) {
return Module.resolve(id, uri)
}
require.async = function(ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid())
return require
}
// Exec factory
var factory = mod.factory
// 工厂函数有返回值,则返回;
// 无返回值,则返回mod.exports
var exports = isFunction(factory) ?
factory(require, mod.exports = {}, mod) :
factory
if (exports === undefined) {
exports = mod.exports
}
// Reduce memory leak
delete mod.factory
mod.exports = exports
mod.status = STATUS.EXECUTED
// Emit `exec` event
emit("exec", mod)
return exports
}
require 函数获取模块并执行模块的工厂函数,获取返回值。require 函数的resolve 方法则是获取对应模块名的绝对url,require 函数的async 方法异步加载依赖并执行回调。对于工厂方法的返回值,如果工厂方法为对象,则这就是exports 的值;or工厂方法有返回值,则为exports 的值;or module.exports 的值为exports 的值。当可以获取到exports 值时,设置状态为executed 。
值得注意的一点:当想要通过给exports 赋值来导出一个对象时
define(function(require,exports,module){
exports ={
add: function(a,b){
return a+b;
}
}
})
是不成功的.我们通过执行上述方法来判断最终导出exports 的值.首先,函数没有返回值,其次,mod.exports为undefined ,最终导出的exports 为undefined 。为什么会出现这种情况呢?是因为js中引用赋值所造成的。js的赋值策略是“按共享传递”,虽说初始时exports === module.exports ,但是当给exports 赋一个对象时,此时exports 指向该对象,module.exports 却仍未初始化,为undefined ,因此会出错。
正确的写法为
define(function(require,exports,module){
module.exports ={
add: function(a,b){
return a+b;
}
}
})
总结
可以说,seajs的核心模块的实现已讲解完毕,见识了不少编码技巧,领略了回调模式的巧妙,以及于细微处的考量。代码的每一处都考虑到了内存泄露和this指针引用偏移的危险,做了积极的预防,这种精神值得学习。
对于seajs,前前后后花了不下一个星期来阅读源码,从刚开始的一知半解到如今的拜服,我真切的领会到了设计思想的重要性。之前我不怎么重视实现的技巧性,认为能够实现,不出bug,健壮性良好即可,但是现在我意识到错了,尤其是在load依赖模块那部分实现中,技巧性十足。以上就是本文的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
|