本文介绍的是seajs模块之间依赖的加载以及模块的执行,下面话不多说直接来看详细的介绍。
seajs模块之间依赖的加载以及模块的执行,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[i] = cachedMods[uris[i]].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[i])
// 修改 依赖文件 的 _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[i]]
// 该依赖并未加载,则先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依赖模块那部分实现中,技巧性十足。以上就是本文的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
本文介绍的是seajs模块之间依赖的加载以及模块的执行,下面话不多说直接来看…
前言
seajs学习之模块的依赖加载及模块API的导出,seajsapi
前言
SeaJS非常强大,SeaJS可以加载任意 JavaScript
模块和css模块样式,SeaJS会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中。
通过参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。
模块类和状态类
首先定义了一个Module类,对应与一个模块
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []
this.exports = null
this.status = 0
// Who depends on me
this._waitings = {}
// The number of unloaded dependencies
this._remain = 0
}
Module有一些属性,uri对应该模块的绝对url,在Module.define
函数中会有介绍;dependencies为依赖模块数组;exports为导出的API;status为当前的状态码;_waitings对象为当前依赖该模块的其他模块哈希表,其中key为其他模块的url;_remain为计数器,记录还未加载的模块个数。
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1,
// 2 - The meta data has been saved to cachedMods
SAVED: 2,
// 3 - The `module.dependencies` are being loaded
LOADING: 3,
// 4 - The module are ready to execute
LOADED: 4,
// 5 - The module is being executed
EXECUTING: 5,
// 6 - The `module.exports` is available
EXECUTED: 6
}
上述为状态对象,记录模块的当前状态:模块初始化状态为0,当加载该模块时,为状态fetching;模块加载完毕并且缓存在cacheMods后,为状态saved;loading状态意味着正在加载该模块的其他依赖模块;loaded表示所有依赖模块加载完毕,执行该模块的回调函数,并设置依赖该模块的其他模块是否还有依赖模块未加载,若加载完毕执行回调函数;executing状态表示该模块正在执行;executed则是执行完毕,可以使用exports的API。
模块的定义
commonJS规范规定用define
函数来定义一个模块。define可以接受1,2,3个参数均可,不过对于Module/wrappings规范而言,module.declare
或者define
函数只能接受一个参数,即工厂函数或者对象。不过原则上接受参数的个数并没有本质上的区别,只不过库在后台给额外添加模块名。
seajs鼓励使用define(function(require,exports,module){})
这种模块定义方式,这是典型的Module/wrappings规范实现。但是在后台通过解析工厂函数的require
方法来获取依赖模块并给模块设置id和url。
// Define a module
Module.define = function (id, deps, factory) {
var argsLen = arguments.length
// define(factory)
if (argsLen === 1) {
factory = id
id = undefined
}
else if (argsLen === 2) {
factory = deps
// define(deps, factory)
if (isArray(id)) {
deps = id
id = undefined
}
// define(id, factory)
else {
deps = undefined
}
}
// Parse dependencies according to the module factory code
// 如果deps为非数组,则序列化工厂函数获取入参。
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
var meta = {
id: id,
uri: Module.resolve(id), // 绝对url
deps: deps,
factory: factory
}
// Try to derive uri in IE6-9 for anonymous modules
// 导出匿名模块的uri
if (!meta.uri && doc.attachEvent) {
var script = getCurrentScript()
if (script) {
meta.uri = script.src
}
// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri
}
// Emit `define` event, used in nocache plugin, seajs node version etc
emit("define", meta)
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
模块定义的最后,通过Module.save
方法,将模块保存到cachedMods缓存体中。
parseDependencies
方法比较巧妙的获取依赖模块。他通过函数的字符串表示,使用正则来获取require(“…”)
中的模块名。
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g
function parseDependencies(code) {
var ret = []
// 此处使用函数序列化(传入的factory)进行字符串匹配,寻找require(“...”)的关键字
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2) {
ret.push(m2)
}
})
return ret
}
异步加载模块
加载模块可以有多种方式,xhr方式可以同步加载,也可以异步加载,但是存在同源问题,因此难以在此使用。另外script tag
方式在IE和现代浏览器下可以保证并行加载和顺序执行,script element
方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。
在seajs中,是采用script element
方式来并行加载js/css资源的,并针对旧版本的webkit浏览器加载css做了hack。
function request(url, callback, charset) {
var isCSS = IS_CSS_RE.test(url)
var node = doc.createElement(isCSS ? "link" : "script")
if (charset) {
var cs = isFunction(charset) ? charset(url) : charset
if (cs) {
node.charset = cs
}
}
// 添加 onload 函数。
addOnload(node, callback, isCSS, url)
if (isCSS) {
node.rel = "stylesheet"
node.href = url
}
else {
node.async = true
node.src = url
}
// For some cache cases in IE 6-8, the script executes IMMEDIATELY after
// the end of the insert execution, so use `currentlyAddingScript` to
// hold current node, for deriving url in `define` call
currentlyAddingScript = node
// ref: #185 & http://dev.jquery.com/ticket/2709
baseElement ?
head.insertBefore(node, baseElement) :
head.appendChild(node)
currentlyAddingScript = null
}
function addOnload(node, callback, isCSS, url) {
var supportOnload = "onload" in node
// for Old WebKit and Old Firefox
if (isCSS && (isOldWebKit || !supportOnload)) {
setTimeout(function() {
pollCss(node, callback)
}, 1) // Begin after node insertion
return
}
if (supportOnload) {
node.onload = onload
node.onerror = function() {
emit("error", { uri: url, node: node })
onload()
}
}
else {
node.onreadystatechange = function() {
if (/loaded|complete/.test(node.readyState)) {
onload()
}
}
}
function onload() {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null
// Remove the script to reduce memory leak
if (!isCSS && !data.debug) {
head.removeChild(node)
}
// Dereference the node
node = null
callback()
}
}
// 针对 旧webkit和不支持onload的CSS节点判断加载完毕的方法
function pollCss(node, callback) {
var sheet = node.sheet
var isLoaded
// for WebKit < 536
if (isOldWebKit) {
if (sheet) {
isLoaded = true
}
}
// for Firefox < 9.0
else if (sheet) {
try {
if (sheet.cssRules) {
isLoaded = true
}
} catch (ex) {
// The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
// to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
// in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
isLoaded = true
}
}
}
setTimeout(function() {
if (isLoaded) {
// Place callback here to give time for style rendering
callback()
}
else {
pollCss(node, callback)
}
}, 20)
}
其中有些细节还需注意,当采用script element
方法插入script节点时,尽量作为首个子节点插入到head中,这是由于一个难以发现的bug:
GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS <BASE HREF> TAG IN THE HEAD
fetch模块
初始化Module对象时,状态为0,该对象对应的js文件并未加载,若要加载js文件,需要使用上节提到的request
方法,但是也不可能仅仅加载该文件,还需要设置module对象的状态及其加载module依赖的其他模块。
这些逻辑在fetch
方法中得以体现:
// Fetch a module
// 加载该模块,fetch函数中调用了seajs.request函数
Module.prototype.fetch = function(requestCache) {
var mod = this
var uri = mod.uri
mod.status = STATUS.FETCHING
// Emit `fetch` event for plugins such as combo plugin
var emitData = { uri: uri }
emit("fetch", emitData)
var requestUri = emitData.requestUri || uri
// Empty uri or a non-CMD module
if (!requestUri || fetchedList[requestUri]) {
mod.load()
return
}
if (fetchingList[requestUri]) {
callbackList[requestUri].push(mod)
return
}
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
// Emit `request` event for plugins such as text plugin
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: data.charset
})
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
// 回调函数
function onRequest() {
delete fetchingList[requestUri]
fetchedList[requestUri] = true
// Save meta data of anonymous module
if (anonymousMeta) {
Module.save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) m.load()
}
}
其中seajs.request
就是上节的request
方法。onRequest
作为回调函数,作用是加载该模块的其他依赖模块。
总结
以上就是seajs模块的依赖加载及模块API的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注帮客之家。
前言 SeaJS非常强大,SeaJS可以加载任意 JavaScript
模块和css模块样式,SeaJS会保证你在使用…
本文实例讲述了seajs中模块依赖的加载处理。分享给大家供大家参考,具体如下:
入口方法
SeaJS非常强大,SeaJS可以加载任意 JavaScript
模块和css模块样式,SeaJS会保证你在使用一个模块时,已经将所依赖的其他模块载入到脚本运行环境中。
最近在做项目的时候发现一些关于模块依赖问题,特记录下:
每个程序都有个入口方法,类似于c的main
函数,seajs也不例外。系列一的demo在首页使用了seajs.use()
,这便是入口方法。入口方法可以接受2个参数,第一个参数为模块名称,第二个为回调函数。入口方法定义了一个新的模块,这个新定义的模块依赖入参提供的模块。然后设置新模块的回调函数,用以在loaded
状态之后调用。该回调函数主要是执行所有依赖模块的工厂函数,最后在执行入口方法提供的回调。
通过参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。
比如现有3个文件:
// 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[i] = cachedMods[uris[i]].exec()
}
// 回调函数的入参对应依赖模块的返回值
if (callback) {
callback.apply(global, exports)
}
delete mod.callback
}
mod.load()
}
模块类和状态类
/*init.js*/
define(function(require, exports, module){
require('jquery');
require('jquery.plugA');
})
/*jquery.plugA.js*/
define(function(require, exports, module){
require('jquery');
require('jquery.plugB');
//code...
})
/*jquery.plugB.js*/
define(functioin(require, exports, module){
require('jquery');
//code...
})
Module.preload
用于预加载seajs提供的插件plugins,非主要功能,可以忽略。而Module.use
则是核心方法,该方法正如之前所说,创建新的module并设置回调函数,最后加载新模块的所有依赖模块。
首先定义了一个Module类,对应与一个模块
比如执行init.js时,init.js、jquery.plugA.js、jquery.plugB.js都会依赖到jquery,那么这种情况下seajs对jquery如何处理的呢?只执行一次?执行多次?还是其他方式?
加载依赖之load方法
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []
this.exports = null
this.status = 0
// Who depends on me
this._waitings = {}
// The number of unloaded dependencies
this._remain = 0
}
此处参考玉伯的回答:
load
方法可谓是seajs的精华所在。该方法主要加载依赖模块并依次执行依赖模块的回调函数,最终执行的回调函数则是通过seajs.use(“./name”)
创建的新模块的回调,也就是mod.callback
。
Module有一些属性,uri对应该模块的绝对url,在Module.define
函数中会有介绍;dependencies为依赖模块数组;exports为导出的API;status为当前的状态码;_waitings对象为当前依赖该模块的其他模块哈希表,其中key为其他模块的url;_remain为计数器,记录还未加载的模块个数。
我对模块调用的理解是,调用是指获取某个模块的接口。在
SeaJS 里,只有 seajs.use, require.async, 和
require 会产生模块调用,比如: var a = require('./a')
在执行 require(‘./a’)
时,会获取模块的接口,如果是第一次调用,会初始化模块
a,以后再调用时,直接返回模块 a 的接口; define
只是注册模块信息,比如打包之后:define(id, deps, factory)
是注册了一个模块到 seajs.cache 中,define
类似:seajs.cache[id] = { id: id, dependencies: deps, factory: factory }
load
方法递归加载依赖模块,如果依赖模块还依赖其他模块,则再加载这个模块。这是通过Module
类中的_waitings
和_remain
来实现的。
var STATUS = Module.STATUS = {
// 1 - The `module.uri` is being fetched
FETCHING: 1,
// 2 - The meta data has been saved to cachedMods
SAVED: 2,
// 3 - The `module.dependencies` are being loaded
LOADING: 3,
// 4 - The module are ready to execute
LOADED: 4,
// 5 - The module is being executed
EXECUTING: 5,
// 6 - The `module.exports` is available
EXECUTED: 6
}
是纯注册信息。
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[i])
// 修改 依赖文件 的 _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[i]]
// 该依赖并未加载,则先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
}
上述为状态对象,记录模块的当前状态:模块初始化状态为0,当加载该模块时,为状态fetching;模块加载完毕并且缓存在cacheMods后,为状态saved;loading状态意味着正在加载该模块的其他依赖模块;loaded表示所有依赖模块加载完毕,执行该模块的回调函数,并设置依赖该模块的其他模块是否还有依赖模块未加载,若加载完毕执行回调函数;executing状态表示该模块正在执行;executed则是执行完毕,可以使用exports的API。
而 require('./a')
时,才会执行 seajs.cache['a'].factory
, 执行后得到
seajs.cache['a'].exports
首先初始化模块的_waitings
和_remain
属性,如果_remain
为0,则意味着没有依赖或者依赖已加载,可以执行onload
函数;如果不为0,则fetch
未加载的模块。在这里有个实现的小技巧,就是同时加载所有依赖:requestCache
对象保存加载函数:(在fetch
函数中定义)
模块的定义
扩展:URI与URL的区别
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
commonJS规范规定用define
函数来定义一个模块。define可以接受1,2,3个参数均可,不过对于Module/wrappings规范而言,module.declare
或者define
函数只能接受一个参数,即工厂函数或者对象。不过原则上接受参数的个数并没有本质上的区别,只不过库在后台给额外添加模块名。
URI:Uniform Resource Identifiers ,统一资源标识符;
其中,sendRequest
函数定义如下:
seajs鼓励使用define(function(require,exports,module){})
这种模块定义方式,这是典型的Module/wrappings规范实现。但是在后台通过解析工厂函数的require
方法来获取依赖模块并给模块设置id和url。
URL:Uniform Resource Locators ,统一资源定位符;
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
// Define a module
Module.define = function (id, deps, factory) {
var argsLen = arguments.length
// define(factory)
if (argsLen === 1) {
factory = id
id = undefined
}
else if (argsLen === 2) {
factory = deps
// define(deps, factory)
if (isArray(id)) {
deps = id
id = undefined
}
// define(id, factory)
else {
deps = undefined
}
}
// Parse dependencies according to the module factory code
// 如果deps为非数组,则序列化工厂函数获取入参。
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
var meta = {
id: id,
uri: Module.resolve(id), // 绝对url
deps: deps,
factory: factory
}
// Try to derive uri in IE6-9 for anonymous modules
// 导出匿名模块的uri
if (!meta.uri && doc.attachEvent) {
var script = getCurrentScript()
if (script) {
meta.uri = script.src
}
// NOTE: If the id-deriving methods above is failed, then falls back
// to use onload event to get the uri
}
// Emit `define` event, used in nocache plugin, seajs node version etc
emit("define", meta)
meta.uri ? Module.save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
URN:Uniform Resource Names,统一资源名称
并行加载所有依赖,当依赖加载完毕,执行onRequest
回调,向上冒泡,加载依赖的依赖,直至没有依赖模块。
模块定义的最后,通过Module.save
方法,将模块保存到cachedMods缓存体中。
URL,URN是URI的子集.
当最上层的依赖已没有依赖模块时,执行onload
函数,在函数体内设置状态为loaded
,执行mod.callback,
并检查并设置该模块的_waitings
属性,判断下层模块是否还有依赖,若没有则执行下层模块的mod.callback
,这一依次回溯,最终将会执行通过seajs.use
创建的匿名模块的mod.callback
。
parseDependencies
方法比较巧妙的获取依赖模块。他通过函数的字符串表示,使用正则来获取require(“…”)
中的模块名。
参考
例证
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g
function parseDependencies(code) {
var ret = []
// 此处使用函数序列化(传入的factory)进行字符串匹配,寻找require(“...”)的关键字
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2) {
ret.push(m2)
}
})
return ret
}
Module.STATUS的具体含义:
分清 URI、URL 和
URN
通过一个简单的例子,论证上述过程:
异步加载模块
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《JavaScript扩展技巧总结》、《JavaScript字符与字符串操作技巧总结》、《JavaScript数学运算用法总结》、《JavaScript中json操作技巧总结》、《JavaScript错误与调试技巧总结》及《JavaScript数据结构与算法技巧总结》
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));
})
加载模块可以有多种方式,xhr方式可以同步加载,也可以异步加载,但是存在同源问题,因此难以在此使用。另外script tag
方式在IE和现代浏览器下可以保证并行加载和顺序执行,script element
方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。
希望本文所述对大家JavaScript程序设计有所帮助。
通过调试工具,可以看出执行onload
的次序:
在seajs中,是采用script element
方式来并行加载js/css资源的,并针对旧版本的webkit浏览器加载css做了hack。
您可能感兴趣的文章:
- seajs1.3.0源码解析之module依赖有序加载
- seajs中最常用的7个功能、配置示例
- SeaJS中use函数用法实例分析
- seajs模块之间依赖的加载以及模块的执行
- 把jQuery的类、插件封装成seajs的模块的方法
- seajs中模块的解析规则详解和模块使用总结
- seaJs的模块定义和模块加载浅析
- SeaJS 与 RequireJS
的差异对比 - Seajs的学习笔记
- Seajs是什么及sea.js
由来,特点以及优势 - seaJs使用心得之exports与module.exports的区别实例分析
function request(url, callback, charset) {
var isCSS = IS_CSS_RE.test(url)
var node = doc.createElement(isCSS ? "link" : "script")
if (charset) {
var cs = isFunction(charset) ? charset(url) : charset
if (cs) {
node.charset = cs
}
}
// 添加 onload 函数。
addOnload(node, callback, isCSS, url)
if (isCSS) {
node.rel = "stylesheet"
node.href = url
}
else {
node.async = true
node.src = url
}
// For some cache cases in IE 6-8, the script executes IMMEDIATELY after
// the end of the insert execution, so use `currentlyAddingScript` to
// hold current node, for deriving url in `define` call
currentlyAddingScript = node
// ref: #185 & http://dev.jquery.com/ticket/2709
baseElement ?
head.insertBefore(node, baseElement) :
head.appendChild(node)
currentlyAddingScript = null
}
function addOnload(node, callback, isCSS, url) {
var supportOnload = "onload" in node
// for Old WebKit and Old Firefox
if (isCSS && (isOldWebKit || !supportOnload)) {
setTimeout(function() {
pollCss(node, callback)
}, 1) // Begin after node insertion
return
}
if (supportOnload) {
node.onload = onload
node.onerror = function() {
emit("error", { uri: url, node: node })
onload()
}
}
else {
node.onreadystatechange = function() {
if (/loaded|complete/.test(node.readyState)) {
onload()
}
}
}
function onload() {
// Ensure only run once and handle memory leak in IE
node.onload = node.onerror = node.onreadystatechange = null
// Remove the script to reduce memory leak
if (!isCSS && !data.debug) {
head.removeChild(node)
}
// Dereference the node
node = null
callback()
}
}
// 针对 旧webkit和不支持onload的CSS节点判断加载完毕的方法
function pollCss(node, callback) {
var sheet = node.sheet
var isLoaded
// for WebKit < 536
if (isOldWebKit) {
if (sheet) {
isLoaded = true
}
}
// for Firefox < 9.0
else if (sheet) {
try {
if (sheet.cssRules) {
isLoaded = true
}
} catch (ex) {
// The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
// to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
// in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
isLoaded = true
}
}
}
setTimeout(function() {
if (isLoaded) {
// Place callback here to give time for style rendering
callback()
}
else {
pollCss(node, callback)
}
}, 20)
}
最后可看出,匿名模块的状态码为4,也就是该模块并未执行.确实,也没有给匿名模块定义工厂函数,无法执行.
其中有些细节还需注意,当采用script element
方法插入script节点时,尽量作为首个子节点插入到head中,这是由于一个难以发现的bug:
模块执行之exec
GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS <BASE HREF> TAG IN THE HEAD
模块执行是在seajs.use
中定义的mod.callback
中调用的,依次调用所有依赖的exec
方法,执行程序逻辑。exec
方法中有commonJS的一些重要关键字或者函数,如require
,exports
等,让我们一看究竟:
fetch模块
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
}
初始化Module对象时,状态为0,该对象对应的js文件并未加载,若要加载js文件,需要使用上节提到的request
方法,但是也不可能仅仅加载该文件,还需要设置module对象的状态及其加载module依赖的其他模块。
require
函数获取模块并执行模块的工厂函数,获取返回值。require
函数的resolve
方法则是获取对应模块名的绝对url,require
函数的async
方法异步加载依赖并执行回调。对于工厂方法的返回值,如果工厂方法为对象,则这就是exports
的值;or工厂方法有返回值,则为exports
的值;or module.exports
的值为exports
的值。当可以获取到exports
值时,设置状态为executed
。
这些逻辑在fetch
方法中得以体现:
值得注意的一点:当想要通过给exports
赋值来导出一个对象时
// Fetch a module
// 加载该模块,fetch函数中调用了seajs.request函数
Module.prototype.fetch = function(requestCache) {
var mod = this
var uri = mod.uri
mod.status = STATUS.FETCHING
// Emit `fetch` event for plugins such as combo plugin
var emitData = { uri: uri }
emit("fetch", emitData)
var requestUri = emitData.requestUri || uri
// Empty uri or a non-CMD module
if (!requestUri || fetchedList[requestUri]) {
mod.load()
return
}
if (fetchingList[requestUri]) {
callbackList[requestUri].push(mod)
return
}
fetchingList[requestUri] = true
callbackList[requestUri] = [mod]
// Emit `request` event for plugins such as text plugin
emit("request", emitData = {
uri: uri,
requestUri: requestUri,
onRequest: onRequest,
charset: data.charset
})
if (!emitData.requested) {
requestCache ?
requestCache[emitData.requestUri] = sendRequest :
sendRequest()
}
function sendRequest() {
seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
// 回调函数
function onRequest() {
delete fetchingList[requestUri]
fetchedList[requestUri] = true
// Save meta data of anonymous module
if (anonymousMeta) {
Module.save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) m.load()
}
}
define(function(require,exports,module){
exports ={
add: function(a,b){
return a+b;
}
}
})
其中seajs.request
就是上节的request
方法。onRequest
作为回调函数,作用是加载该模块的其他依赖模块。
是不成功的.我们通过执行上述方法来判断最终导出exports
的值.首先,函数没有返回值,其次,mod.exports为undefined
,最终导出的exports
为undefined
。为什么会出现这种情况呢?是因为js中引用赋值所造成的。js的赋值策略是“按共享传递”,虽说初始时exports === module.exports
,但是当给exports
赋一个对象时,此时exports
指向该对象,module.exports
却仍未初始化,为undefined
,因此会出错。
总结
正确的写法为
以上就是seajs模块的依赖加载及模块API的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注脚本之家。
define(function(require,exports,module){
module.exports ={
add: function(a,b){
return a+b;
}
}
})
您可能感兴趣的文章:
- seajs模块压缩问题与解决方法实例分析
- 把jQuery的类、插件封装成seajs的模块的方法
- seajs模块之间依赖的加载以及模块的执行
- 基于gulp合并压缩Seajs模块的方式说明
- 深入探寻seajs的模块化与加载方式
- seajs中模块的解析规则详解和模块使用总结
- seajs中最常用的7个功能、配置示例
总结
可以说,seajs的核心模块的实现已讲解完毕,见识了不少编码技巧,领略了回调模式的巧妙,以及于细微处的考量。代码的每一处都考虑到了内存泄露和this指针引用偏移的危险,做了积极的预防,这种精神值得学习。
对于seajs,前前后后花了不下一个星期来阅读源码,从刚开始的一知半解到如今的拜服,我真切的领会到了设计思想的重要性。之前我不怎么重视实现的技巧性,认为能够实现,不出bug,健壮性良好即可,但是现在我意识到错了,尤其是在load依赖模块那部分实现中,技巧性十足。以上就是本文的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
您可能感兴趣的文章:
- SeaJS 与 RequireJS
的差异对比 - LABjs、RequireJS、SeaJS的区别
- SeaJS入门教程系列之使用SeaJS(二)
- SeaJS入门教程系列之SeaJS介绍(一)
- SeaJS入门教程系列之完整示例(三)
- seajs中模块的解析规则详解和模块使用总结
- seaJs的模块定义和模块加载浅析
- Seajs的学习笔记
- seajs1.3.0源码解析之module依赖有序加载
- seajs下require书写约定实例分析