读RequireJS — 加载系统

RequireJS是一个模块加载框架,所以为了最直观的感受它,我们先来看看它的加载系统。


先看入口方法:

/**
 * Does the request to load a module for the browser case.
 * Make this a separate function to allow other environments
 * to override it.
 *
 * @param {Object} context the require context to find state.
 * @param {String} moduleName the name of the module.
 * @param {Object} url the URL to the module.
 */
req.load = function (context, moduleName, url) {
	req.resourcesReady(false);

	context.scriptCount += 1;
	req.attach(url, context, moduleName);

	//If tracking a jQuery, then make sure its ready callbacks
	//are put on hold to prevent its ready callbacks from
	//triggering too soon.
	if (context.jQuery && !context.jQueryIncremented) {
		jQueryHoldReady(context.jQuery, true);
		context.jQueryIncremented = true;
	}
};

解释一下:

加载的脚本是有一个运行环境的,这样才可以避免冲突,context 即这个环境,暂且理解为一个 object 就行了,加载一个脚本就让 context.scriptCount 加1(表示loading中的脚本增加了一个,加载完后,context.scriptCount 会减1),然后进到attach()

 

/**
 * Attaches the script represented by the URL to the current
 * environment. Right now only supports browser loading,
 * but can be redefined in other environments to do the right thing.
 * @param {String} url the url of the script to attach.
 * @param {Object} context the context that wants the script.
 * @param {moduleName} the name of the module that is associated with the script.
 * @param {Function} [callback] optional callback, defaults to require.onScriptLoad
 * @param {String} [type] optional type, defaults to text/javascript
 * @param {Function} [fetchOnlyFunction] optional function to indicate the script node
 * should be set up to fetch the script but do not attach it to the DOM
 * so that it can later be attached to execute it. This is a way for the
 * order plugin to support ordered loading in IE. Once the script is fetched,
 * but not executed, the fetchOnlyFunction will be called.
 */
req.attach = function (url, context, moduleName, callback, type, fetchOnlyFunction) {
	var node;
	if (isBrowser) {
		//In the browser so use a script tag
		callback = callback || req.onScriptLoad;
		node = context && context.config && context.config.xhtml ?
				document.createElementNS("http://www.w3.org/1999/xhtml", "html:script") :
				document.createElement("script");
		node.type = type || "text/javascript";
		node.charset = "utf-8";
		//Use async so Gecko does not block on executing the script if something
		//like a long-polling comet tag is being run first. Gecko likes
		//to evaluate scripts in DOM order, even for dynamic scripts.
		//It will fetch them async, but only evaluate the contents in DOM
		//order, so a long-polling script tag can delay execution of scripts
		//after it. But telling Gecko we expect async gets us the behavior
		//we want -- execute it whenever it is finished downloading. Only
		//Helps Firefox 3.6+
		//Allow some URLs to not be fetched async. Mostly helps the order!
		//plugin
		node.async = !s.skipAsync[url];

		if (context) {
			node.setAttribute("data-requirecontext", context.contextName);
		}
		node.setAttribute("data-requiremodule", moduleName);

		//Set up load listener. Test attachEvent first because IE9 has
		//a subtle issue in its addEventListener and script onload firings
		//that do not match the behavior of all other browsers with
		//addEventListener support, which fire the onload event for a
		//script right after the script execution. See:
		//https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
		//UNFORTUNATELY Opera implements attachEvent but does not follow the script
		//script execution mode.
		if (node.attachEvent && !isOpera) {
			//Probably IE. IE (at least 6-8) do not fire
			//script onload right after executing the script, so
			//we cannot tie the anonymous define call to a name.
			//However, IE reports the script as being in "interactive"
			//readyState at the time of the define call.
			useInteractive = true;


			if (fetchOnlyFunction) {
				//Need to use old school onreadystate here since
				//when the event fires and the node is not attached
				//to the DOM, the evt.srcElement is null, so use
				//a closure to remember the node.
				node.onreadystatechange = function (evt) {
					//Script loaded but not executed.
					//Clear loaded handler, set the real one that
					//waits for script execution.
					if (node.readyState === 'loaded') {
						node.onreadystatechange = null;
						node.attachEvent("onreadystatechange", callback);
						fetchOnlyFunction(node);
					}
				};
			} else {
				node.attachEvent("onreadystatechange", callback);
			}
		} else {
			node.addEventListener("load", callback, false);
		}
		node.src = url;

		//Fetch only means waiting to attach to DOM after loaded.
		if (!fetchOnlyFunction) {
			req.addScriptToDom(node);
		}

		return node;
	} else if (isWebWorker) {
		//In a web worker, use importScripts. This is not a very
		//efficient use of importScripts, importScripts will block until
		//its script is downloaded and evaluated. However, if web workers
		//are in play, the expectation that a build has been done so that
		//only one script needs to be loaded anyway. This may need to be
		//reevaluated if other use cases become common.
		importScripts(url);

		//Account for anonymous modules
		context.completeLoad(moduleName);
	}
	return null;
};

解释一下:

RequireJS 的运行环境可以是浏览器,也可以是WebWorker,对于后者我完全木有概念,直接无视,所以这里只说第一个分支。

1. 创建一个 script 节点

2. node.async = !s.skipAsync[url]; 出处如下:

s = req.s = {
    contexts: contexts,
    //Stores a list of URLs that should not get async script tag treatment.
    skipAsync: {}
};

3. 接下来的两句比较重要

  如果传入context,node.setAttribute("data-requirecontext", context.contextName);

  node.setAttribute("data-requiremodule", moduleName);

  这样处理之后,当脚本完成加载时,才好对号入座。

4. 接着就是侦听加载事件了,首先处理IE6-9,这里有段注释,我还是翻译一下吧:

  首先检测attachEvent,你肯定会想当然的认为是针对IE6-8,其实也包括IE9,因为IE9的 addEventListener 和 script onload 触发机制有个小问题,和别的支持 addEventListener 方法的浏览器的行为不太一致。悲剧的是,Opera支持attachEvent,但却不遵循IE这套机制。

  注释提供的链接失效了, 可参考 Franky 的 又说 动态加载 script. ie 下 script Element 的 readyState状态 和 IE9的特性变化(收集贴) , 但这两篇文章都没有涉及注释中提到的问题, 谁能告诉我IE9到底肿么了?

5. 关于useInteractive,注释是这么写的:

  IE专用,至少是IE6-8(我猜还包括9),在脚本执行完之后不会立即发出 onload 事件,所以匿名的 define() 无法指定moduleName。但是,在 define() 执行期间,IE会报告对应的 script.readyState 值为 "interactive",所以我们可以通过这个特性拿到 script 节点,并获取 moduleName。

  按我自己的话说一遍吧,匿名模块的处理,在标准浏览器中,是通过 onload 事件去取 script 节点的 data-requiremodule 属性;在IE中,因为不会触发 onload 事件,所以处理提前到 define() 执行时,通过 "interactive" 特性取到。

6. 解释一下 fetchOnlyFunction 参数,注释是这么写的:

  此参数可选。表示用 script 节点获取脚本,但先别把它加入DOM,而是等脚本加载完成后,再加入DOM,这时它才开始执行。order插件实现IE中的顺序加载就是使用这种方式。一旦脚本获取到了,却还没执行,这时就会调用fetchOnlyFunction。

  需要注意一下,我严重怀疑所谓的 fetchOnly 是针对IE的。非IE浏览器在 script 节点未append进DOM时,连请求都不会发。如果那位大侠看懂了这个参数的意义,拜托一定要告诉我啊!!

7. 这里使用了一个小技巧:当 script 节点未加入 DOM 时(即传入了 fetchOnlyFunction 参数的情况),如果事件侦听使用 node.attachEvent 方式,那么在事件处理函数中 event.srcElement 为null。经我测试,确实如此,所以这里使用了闭包,这样才能取到 node。


这里涉及到 addScriptToDom()

/**
 * Adds a node to the DOM. Public function since used by the order plugin.
 * This method should not normally be called by outside code.
 */
req.addScriptToDom = function (node) {
	//For some cache cases in IE 6-8, the script executes before the end
	//of the appendChild execution, so to tie an anonymous define
	//call to the module name (which is stored on the node), hold on
	//to a reference to this node, but clear after the DOM insertion.
	currentlyAddingScript = node;
	if (baseElement) {
		head.insertBefore(node, baseElement);
	} else {
		head.appendChild(node);
	}
	currentlyAddingScript = null;
};

注释说了,这个方法是给 order 插件用的,外部的代码别用它

稍微解释一下 currentlyAddingScript

  它是给IE 6-8 用的,因为在某些缓存影响下,脚本会在 appendChild 执行结束之前就开始执行,也就是说,那个时刻 script 节点尚未存在于DOM树中,通过 "interactive" 特性也拿不到节点,所以对于匿名模块来说,无法获取moduleName,于是这里先存一下节点,保证有办法拿到它,插入DOM后再清除,因为那个时候可以通过 "interactive" 特性获取。

对于 baseElement,出处如下:

head = s.head = document.getElementsByTagName("head")[0];
//If BASE tag is in play, using appendChild is a problem for IE6.
//When that browser dies, this can be removed. Details in this jQuery bug:
//http://dev.jquery.com/ticket/2709
baseElement = document.getElementsByTagName("base")[0];
if (baseElement) {
	head = s.head = baseElement.parentNode;
}

可见,baseElement 就是 <base> 标签,这里提到了 IE6 的一个bug:

首先需明确的是: <base> 标签必须位于 head 元素内部。

如果 <base> 是自闭合标签,如<base href=""/>,head.appendChild(script),这样 script.parentNode 是 base 而不是 head。为什么会这样呢?因为 IE6 中的 base 会把后面的节点通通归入自己内部,甚至包括 body 都被它收编了,所以 RequireJS 的做法是插到 base 前面。

如果 <base> 不是自闭合标签,如<base href=""></base>,则不存在这个bug

 
attach 方法设置了一个默认回调函数 req.onScriptLoad

/**
 * callback for script loads, used to check status of loading.
 *
 * @param {Event} evt the event from the browser for the script
 * that was loaded.
 *
 * @private
 */
req.onScriptLoad = function (evt) {
	//Using currentTarget instead of target for Firefox 2.0's sake. Not
	//all old browsers will be supported, but this one was easy enough
	//to support and still makes sense.
	var node = evt.currentTarget || evt.srcElement, contextName, moduleName,
		context;

	if (evt.type === "load" || (node && readyRegExp.test(node.readyState))) {
		//Reset interactive script so a script node is not held onto for
		//to long.
		interactiveScript = null;

		//Pull out the name of the module and the context.
		contextName = node.getAttribute("data-requirecontext");
		moduleName = node.getAttribute("data-requiremodule");
		context = contexts[contextName];

		contexts[contextName].completeLoad(moduleName);

		//Clean up script binding. Favor detachEvent because of IE9
		//issue, see attachEvent/addEventListener comment elsewhere
		//in this file.
		if (node.detachEvent && !isOpera) {
			//Probably IE. If not it will throw an error, which will be
			//useful to know.
			node.detachEvent("onreadystatechange", req.onScriptLoad);
		} else {
			node.removeEventListener("load", req.onScriptLoad, false);
		}
	}
};

1. 第一句没用 target,而是 currentTarget,其实在上面这种情况下,target 等价于 currentTarget,所以写哪个都无所谓,但要兼容 FF2,所以这里写了currentTarget

2. 说下 interactiveScript

  这是针对IE的hack,interactive 状态表示脚本正在执行中。在IE中,每加载一个新的script,都会更新 interactiveScript 变量的值,过程是这样的(记住是IE中的情况):

a. 初始化时,interactiveScript为null

b. 加载好一个脚本时,在执行 define() 时获取 interactiveScript,为 null 则遍历 script 节点,总之保证最后有值

c. 在 onScriptLoad 执行时,及时把 interactiveScript 清除

3. 接着从 node 取出 contextName 和 moduleName

4. contexts[contextName].completeLoad(moduleName);

  关于completeLoad,请见我的另一篇文章:通过一个例子读懂 RequireJS

5. 移除事件绑定


 

关于脚本动态加载,推荐以下几篇文章:

非阻塞式JavaScript脚本介绍

DOM Ready 详解

The best way to load external javascript

onload次序测试

模块加载器获取URL的原理

IE6的base标签导致页面结构大混乱

posted @ 2011-12-30 13:23  越己  阅读(4864)  评论(2编辑  收藏  举报