读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. 移除事件绑定
关于脚本动态加载,推荐以下几篇文章: