前言
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的导出的全部内容了,小编会在下一节,将介绍模块之间依赖的加载以及模块的执行。感兴趣的朋友们可以继续关注代码网。
发表评论