读RequireJS — 加载系统
来源:广州中睿信息技术有限公司官网
发布时间:2012/10/21 23:25:16 编辑:admin 阅读 1278
RequireJS是一个模块加载框架,所以为了最直观的感受它,我们先来看看它的加载系统。先看入口方法:/***Doestherequesttoloadamoduleforthebrowsercase

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标签导致页面结构大混乱

联系我们CONTACT 扫一扫
愿景:成为最专业的软件研发服务领航者
中睿信息技术有限公司 广州•深圳 Tel:020-38931912 务实 Pragmatic
广州:广州市天河区翰景路1号金星大厦18层中睿信息 Fax:020-38931912 专业 Professional
深圳:深圳市福田区车公庙有色金属大厦509~510 Tel:0755-25855012 诚信 Integrity
所有权声明:PMI, PMP, Project Management Professional, PMI-ACP, PMI-PBA和PMBOK是项目管理协会(Project Management Institute, Inc.)的注册标志。
版权所有:广州中睿信息技术有限公司 粤ICP备13082838号-2