引言

SeaJS 的核心优势在于,它能够加载任意 JavaScript 模块甚至 CSS 样式表,并确保在调用某个模块时,其所有依赖都已安全无误地加载完成。这背后究竟采用了哪些技术手段?借助上一节的示例,我们直接深入源码一探究竟——那些看似简单的 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 是一个哈希表,存储那些依赖于当前模块的其他模块的 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 函数来定义模块。该函数可接受 1 到 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|(?:^|[^$])\brequires\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 做了特殊兼容处理。
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 & https://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 的第一个子节点之前。原因是 IE6 下存在一个不易察觉的 Bug——当页面 head 中包含 标签时,globalEval 的执行会出现异常。
fetch 模块:加载与依赖处理
初始化 Module 对象时状态为 0,对应的 JS 文件尚未加载。要加载该文件,就需要使用上面提到的 request 方法,但不仅仅要加载文件,还需更新模块状态并处理依赖关系。这些逻辑全部封装在 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 导出的核心实现原理。后续章节将进一步分析模块间依赖的加载机制及模块执行流程。
