Skip to content

Instantly share code, notes, and snippets.

@hudidit
Last active August 29, 2015 14:01
Show Gist options
  • Save hudidit/f58217d79839291f2169 to your computer and use it in GitHub Desktop.
Save hudidit/f58217d79839291f2169 to your computer and use it in GitHub Desktop.
学习 Sea.js 源码,里面的中文注释是我的笔记。
/**
* Sea.js 2.1.0 | seajs.org/LICENSE.md
*/
(function(global, undefined) {
// Avoid conflicting when `sea.js` is loaded multiple times
if (global.seajs) {
return
}
var seajs = global.seajs = {
// The current version of Sea.js being used
version: "2.1.0"
}
var data = seajs.data = {}
/**
* util-lang.js - The minimal language enhancement
*/
function isType(type) {
return function(obj) {
return Object.prototype.toString.call(obj) === "[object " + type + "]"
}
}
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var _cid = 0 // _cid 干嘛的?
function cid() {
return _cid++
}
/**
* util-events.js - The minimal events support
*/
/*
events 对象的结构如下:
events [Object]
|__ event1 [Array]
|__ callback1 [Function]
|__ callback2 [Function]
|__ ...
|__ event2 [Array]
|__ ...
*/
var events = data.events = {}
// Bind event
seajs.on = function(name, callback) {
var list = events[name] || (events[name] = []) // list 是 evnets[name] 这个 callback 数组的引用
list.push(callback) // 添加一个 callback
return seajs // return seajs 是为了链式调用 ( chainable )
}
// Remove event. If `callback` is undefined, remove all callbacks for the
// event. If `event` and `callback` are both undefined, remove all callbacks
// for all events
seajs.off = function(name, callback) {
// Remove *all* events
if (!(name || callback)) {
events = data.events = {}
return seajs
}
var list = events[name]
if (list) {
if (callback) {
for (var i = list.length - 1; i >= 0; i--) {
if (list[i] === callback) {
list.splice(i, 1)
}
}
}
else {
delete events[name] // 如果没有指定 `callback`,停止监听 `name` 事件
}
}
return seajs
}
// Emit event, firing all bound callbacks. Callbacks are passed the same
// arguments as `emit` is, apart from the event name
var emit = seajs.emit = function(name, data) {
var list = events[name], fn
if (list) {
// Copy callback lists to prevent modification
list = list.slice()
// Execute event callbacks
while ((fn = list.shift())) {
fn(data)
}
}
return seajs
}
/**
* util-path.js - The utilities for operating path such as id, uri
*/
var DIRNAME_RE = /[^?#]*\//
var DOT_RE = /\/\.\//g
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\//
// Extract the directory portion of a path
// dirname("a/b/c.js?t=123#xx/zz") ==> "a/b/"
// ref: http://jsperf.com/regex-vs-split/2
function dirname(path) {
return path.match(DIRNAME_RE)[0]
}
// Canonicalize a path
// realpath("http://test.com/a//./b/../c") ==> "http://test.com/a/c"
function realpath(path) {
// /a/b/./c/./d ==> /a/b/c/d
path = path.replace(DOT_RE, "/")
// a/b/c/../../d ==> a/b/../d ==> a/d
while (path.match(DOUBLE_DOT_RE)) {
path = path.replace(DOUBLE_DOT_RE, "/")
}
return path
}
// Normalize an id
// normalize("path/to/a") ==> "path/to/a.js"
// NOTICE: substring is faster than negative slice and RegExp
function normalize(path) {
var last = path.length - 1
// If the uri ends with `#`, just return it without '#'
if (path.charAt(last) === "#") {
return path.substring(0, last)
}
return (path.substring(last - 2) === ".js" ||
path.indexOf("?") > 0 ||
path.substring(last - 3) === ".css") ? path : path + ".js"
}
var PATHS_RE = /^([^/:]+)(\/.+)$/
var VARS_RE = /{([^{]+)}/g
function parseAlias(id) {
var alias = data.alias
return alias && isString(alias[id]) ? alias[id] : id
}
function parsePaths(id) {
var paths = data.paths
var m
if (paths && (m = id.match(PATHS_RE)) && isString(paths[m[1]])) {
id = paths[m[1]] + m[2]
}
return id
}
function parseVars(id) {
var vars = data.vars
if (vars && id.indexOf("{") > -1) {
id = id.replace(VARS_RE, function(m, key) {
return isString(vars[key]) ? vars[key] : m
})
}
return id
}
function parseMap(uri) {
var map = data.map
var ret = uri
if (map) {
for (var i = 0, len = map.length; i < len; i++) {
var rule = map[i]
ret = isFunction(rule) ?
(rule(uri) || uri) :
uri.replace(rule[0], rule[1])
// Only apply the first matched rule
if (ret !== uri) break
}
}
return ret
}
var ABSOLUTE_RE = /^\/\/.|:\//
var ROOT_DIR_RE = /^.*?\/\/.*?\//
function addBase(id, refUri) {
var ret
var first = id.charAt(0)
// Absolute
if (ABSOLUTE_RE.test(id)) {
ret = id
}
// Relative
else if (first === ".") {
ret = realpath((refUri ? dirname(refUri) : data.cwd) + id)
}
// Root
else if (first === "/") {
var m = data.cwd.match(ROOT_DIR_RE)
ret = m ? m[0] + id.substring(1) : id
}
// Top-level
else {
ret = data.base + id
}
return ret
}
function id2Uri(id, refUri) {
if (!id) return ""
id = parseAlias(id)
id = parsePaths(id)
id = parseVars(id)
id = normalize(id)
var uri = addBase(id, refUri)
uri = parseMap(uri)
return uri
}
var doc = document
var loc = location
var cwd = dirname(loc.href)
var scripts = doc.getElementsByTagName("script")
// Recommend to add `seajsnode` id for the `sea.js` script element
var loaderScript = doc.getElementById("seajsnode") ||
scripts[scripts.length - 1]
// When `sea.js` is inline, set loaderDir to current working directory
var loaderDir = dirname(getScriptAbsoluteSrc(loaderScript) || cwd)
function getScriptAbsoluteSrc(node) {
return node.hasAttribute ? // non-IE6/7
node.src :
// see http://msdn.microsoft.com/en-us/library/ms536429(VS.85).aspx
node.getAttribute("src", 4)
}
/**
* util-request.js - The utilities for requesting script and style files
* ref: tests/research/load-js-css/test.html
*/
var head = doc.getElementsByTagName("head")[0] || doc.documentElement
var baseElement = head.getElementsByTagName("base")[0]
var IS_CSS_RE = /\.css(?:\?|$)/i
var READY_STATE_RE = /^(?:loaded|complete|undefined)$/
var currentlyAddingScript
var interactiveScript
// `onload` event is supported in WebKit < 535.23 and Firefox < 9.0
// ref:
// - https://bugs.webkit.org/show_activity.cgi?id=38995
// - https://bugzilla.mozilla.org/show_bug.cgi?id=185236
// - https://developer.mozilla.org/en/HTML/Element/link#Stylesheet_load_events
var isOldWebKit = (navigator.userAgent
.replace(/.*AppleWebKit\/(\d+)\..*/, "$1")) * 1 < 536
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
}
}
addOnload(node, callback, isCSS)
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) {
var missingOnload = isCSS && (isOldWebKit || !("onload" in node))
// for Old WebKit and Old Firefox
if (missingOnload) {
setTimeout(function() {
pollCss(node, callback)
}, 1) // Begin after node insertion
return
}
node.onload = node.onerror = node.onreadystatechange = function() {
if (READY_STATE_RE.test(node.readyState)) {
// 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()
}
}
}
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)
}
function getCurrentScript() {
if (currentlyAddingScript) {
return currentlyAddingScript
}
// For IE6-9 browsers, the script onload event may not fire right
// after the the script is evaluated. Kris Zyp found that it
// could query the script nodes and the one that is in "interactive"
// mode indicates the current script
// ref: http://goo.gl/JHfFW
if (interactiveScript && interactiveScript.readyState === "interactive") {
return interactiveScript
}
var scripts = head.getElementsByTagName("script")
for (var i = scripts.length - 1; i >= 0; i--) {
var script = scripts[i]
if (script.readyState === "interactive") {
interactiveScript = script
return interactiveScript
}
}
}
/**
* util-deps.js - The parser for dependencies
* ref: tests/research/parse-dependencies/test.html
*/
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g
function parseDependencies(code) {
var ret = []
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2) {
ret.push(m2)
}
})
return ret
}
/**
* module.js - The core of module loader
*/
var cachedMods = seajs.cache = {}
var anonymousMeta
var fetchingList = {}
var fetchedList = {}
var callbackList = {}
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
}
function Module(uri, deps) {
this.uri = uri
this.dependencies = deps || []
this.exports = null
this.status = 0
// Who depend on me
this._waitings = {}
// The number of unloaded dependencies
this._remain = 0
}
// Resolve module.dependencies
Module.prototype.resolve = function() {
var mod = this
var ids = mod.dependencies
var uris = []
for (var i = 0, len = ids.length; i < len; i++) {
uris[i] = resolve(ids[i], mod.uri)
}
return uris
}
// Load module.dependencies and fire onload when all done
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)
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])
if (m.status < STATUS.LOADED) {
// Maybe duplicate
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]]
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]()
}
}
}
// Call this method when module is loaded
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
if (mod.callback) {
mod.callback()
}
// 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
}
// Fetch a module
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() {
request(emitData.requestUri, emitData.onRequest, emitData.charset)
}
function onRequest() {
delete fetchingList[requestUri]
fetchedList[requestUri] = true
// Save meta data of anonymous module
if (anonymousMeta) {
save(uri, anonymousMeta)
anonymousMeta = null
}
// Call callbacks
var m, mods = callbackList[requestUri]
delete callbackList[requestUri]
while ((m = mods.shift())) m.load()
}
}
// Execute a module
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 cachedMods[require.resolve(id)].exec()
}
require.resolve = function(id) {
return resolve(id, uri)
}
require.async = function(ids, callback) {
Module.use(ids, callback, uri + "_async_" + cid())
return require
}
// Exec factory
var factory = mod.factory
var exports = isFunction(factory) ?
factory(require, mod.exports = {}, mod) :
factory
if (exports === undefined) {
exports = mod.exports
}
// Emit `error` event
if (exports === null && !IS_CSS_RE.test(uri)) {
emit("error", mod)
}
// Reduce memory leak
delete mod.factory
mod.exports = exports
mod.status = STATUS.EXECUTED
// Emit `exec` event
emit("exec", mod)
return exports
}
// 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
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
var meta = {
id: id,
uri: resolve(id),
deps: deps,
factory: factory
}
// Try to derive uri in IE6-9 for anonymous modules
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 ? save(meta.uri, meta) :
// Save information for "saving" work in the script onload event
anonymousMeta = meta
}
// Get an existed module or create a new one
Module.get = function(uri, deps) {
return cachedMods[uri] || (cachedMods[uri] = new Module(uri, deps))
}
// 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()
}
// 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()
}
}
// Helpers
function resolve(id, refUri) {
// Emit `resolve` event for plugins such as text plugin
var emitData = { id: id, refUri: refUri }
emit("resolve", emitData)
return emitData.uri || id2Uri(emitData.id, refUri)
}
function save(uri, meta) {
var mod = Module.get(uri)
// Do NOT override already saved modules
if (mod.status < STATUS.SAVED) {
mod.id = meta.id || uri
mod.dependencies = meta.deps || []
mod.factory = meta.factory
mod.status = STATUS.SAVED
}
}
// Public API
seajs.use = function(ids, callback) {
Module.preload(function() {
Module.use(ids, callback, data.cwd + "_use_" + cid())
})
return seajs
}
Module.define.cmd = {}
global.define = Module.define
// For Developers
seajs.Module = Module
data.fetchedList = fetchedList
data.cid = cid
seajs.resolve = id2Uri
seajs.require = function(id) {
return (cachedMods[resolve(id)] || {}).exports
}
/**
* config.js - The configuration for the loader
*/
var BASE_RE = /^(.+?\/)(\?\?)?(seajs\/)+/
// The root path to use for id2uri parsing
// If loaderUri is `http://test.com/libs/seajs/[??][seajs/1.2.3/]sea.js`, the
// baseUri should be `http://test.com/libs/`
data.base = (loaderDir.match(BASE_RE) || ["", loaderDir])[1]
// The loader directory
data.dir = loaderDir
// The current working directory
data.cwd = cwd
// The charset for requesting files
data.charset = "utf-8"
// Modules that are needed to load before all other modules
data.preload = (function() {
var plugins = []
// Convert `seajs-xxx` to `seajs-xxx=1`
// NOTE: use `seajs-xxx=1` flag in uri or cookie to preload `seajs-xxx`
var str = loc.search.replace(/(seajs-\w+)(&|$)/g, "$1=1$2")
// Add cookie string
str += " " + doc.cookie
// Exclude seajs-xxx=0
str.replace(/(seajs-\w+)=1/g, function(m, name) {
plugins.push(name)
})
return plugins
})()
// data.alias - An object containing shorthands of module id
// data.paths - An object containing path shorthands in module id
// data.vars - The {xxx} variables in module id
// data.map - An array containing rules to map module uri
// data.debug - Debug mode. The default value is false
seajs.config = function(configData) {
for (var key in configData) {
var curr = configData[key]
var prev = data[key]
// Merge object config such as alias, vars
if (prev && isObject(prev)) {
for (var k in curr) {
prev[k] = curr[k]
}
}
else {
// Concat array config such as map, preload
if (isArray(prev)) {
curr = prev.concat(curr)
}
// Make sure that `data.base` is an absolute path
else if (key === "base") {
(curr.slice(-1) === "/") || (curr += "/")
curr = addBase(curr)
}
// Set config
data[key] = curr
}
}
emit("config", configData)
return seajs
}
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment