ext/src/class/Loader.js
主要方法,require,如下:
require: function(expressions, fn, scope, excludes) { var excluded = {}, included = {}, excludedClassNames = [], possibleClassNames = [], classNames = [], references = [], callback, syncModeEnabled, filePath, expression, exclude, className, possibleClassName, i, j, ln, subLn; // 判断是否是例外 if (excludes) { // Convert possible single string to an array. excludes = (typeof excludes === 'string') ? [ excludes ] : excludes; for (i = 0,ln = excludes.length; i < ln; i++) { exclude = excludes[i]; if (typeof exclude == 'string' && exclude.length > 0) { excludedClassNames = Manager.getNamesByExpression(exclude); for (j = 0,subLn = excludedClassNames.length; j < subLn; j++) { excluded[excludedClassNames[j]] = true; } } } } // Convert possible single string to an array. expressions = (typeof expressions === 'string') ? [ expressions ] : (expressions ? expressions : []); // perfect /** * 如果回调方法,定义了参数,需要把相关对象的引用传递回去,见注释1 */ if (fn) { if (fn.length > 0) { callback = function() { var classes = [], i, ln; for (i = 0,ln = references.length; i < ln; i++) { classes.push(Manager.get(references[i])); } return fn.apply(this, classes); }; } else { callback = fn; } } else { callback = Ext.emptyFn; } scope = scope || Ext.global; for (i = 0,ln = expressions.length; i < ln; i++) { expression = expressions[i]; if (typeof expression == 'string' && expression.length > 0) { possibleClassNames = Manager.getNamesByExpression(expression); subLn = possibleClassNames.length; for (j = 0; j < subLn; j++) { possibleClassName = possibleClassNames[j]; if (excluded[possibleClassName] !== true) { references.push(possibleClassName); if (!Manager.isCreated(possibleClassName) && !included[possibleClassName]) { included[possibleClassName] = true; classNames.push(possibleClassName); } } } } } // If the dynamic dependency feature is not being used, throw an error // if the dependencies are not defined if (classNames.length > 0) { if (!Loader.config.enabled) { throw new Error("Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " + "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', ')); } } else { callback.call(scope); return Loader; } syncModeEnabled = Loader.syncModeEnabled; // 不是顺序加载 if (!syncModeEnabled) { queue.push({ requires: classNames.slice(), // this array will be modified as the queue is processed,相当于数组复制 // so we need a copy of it callback: callback, scope: scope }); } ln = classNames.length; for (i = 0; i < ln; i++) { className = classNames[i]; filePath = Loader.getPath(className); // 根据类名,获取绝对路径 // If we are synchronously loading a file that has already been asychronously loaded before // we need to destroy the script tag and revert the count // This file will then be forced loaded in synchronous if (syncModeEnabled && isClassFileLoaded.hasOwnProperty(className)) { if (!isClassFileLoaded[className]) { Loader.numPendingFiles--; Loader.removeScriptElement(filePath); delete isClassFileLoaded[className]; } } if (!isClassFileLoaded.hasOwnProperty(className)) { // 注释2 isClassFileLoaded[className] = false; classNameToFilePathMap[className] = filePath; Loader.numPendingFiles++; Loader.loadScriptFile( filePath, pass(Loader.onFileLoaded, [className, filePath], Loader),// 注释3 pass(Loader.onFileLoadError, [className, filePath], Loader), Loader, syncModeEnabled ); // 注释4 } } if (syncModeEnabled) { callback.call(scope); if (ln === 1) { return Manager.get(className); } } return Loader; }
pass方法,如下:
/** * Create a new function from the provided `fn`, the arguments of which are pre-set to `args`. * New arguments passed to the newly created callback when it's invoked are appended after the pre-set ones. * This is especially useful when creating callbacks. * * For example: * * var originalFunction = function(){ * alert(Ext.Array.from(arguments).join(' ')); * }; * * var callback = Ext.Function.pass(originalFunction, ['Hello', 'World']); * * callback(); // alerts 'Hello World' * callback('by Me'); // alerts 'Hello World by Me' * * {@link Ext#pass Ext.pass} is alias for {@link Ext.Function#pass Ext.Function.pass} * * @param {Function} fn The original function * @param {Array} args The arguments to pass to new callback * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. * @return {Function} The new callback function */ pass: function(fn, args, scope) { if (!Ext.isArray(args)) { if (Ext.isIterable(args)) { args = Ext.Array.clone(args); } else { args = args !== undefined ? [args] : []; } } return function() { var fnArgs = [].concat(args); fnArgs.push.apply(fnArgs, arguments); return fn.apply(scope || this, fnArgs); }; },
loadScriptFile方法,如下:
loadScriptFile: function(url, onLoad, onError, scope, synchronous) { if (isFileLoaded[url]) { return Loader; } var config = Loader.getConfig(), noCacheUrl = url + (config.disableCaching ? ('?' + config.disableCachingParam + '=' + Ext.Date.now()) : ''), // 如果不允许缓存的话,添加时间戳 isCrossOriginRestricted = false, xhr, status, onScriptError, debugSourceURL = ""; scope = scope || Loader; Loader.isLoading = true; if (!synchronous) { // 异步加载 onScriptError = function() { onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous); }; scriptElements[url] = Loader.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope); // 注释5 } else { // 控制顺序加载 if (typeof XMLHttpRequest != 'undefined') { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject('Microsoft.XMLHTTP'); } try { xhr.open('GET', noCacheUrl, false); xhr.send(null); } catch (e) { isCrossOriginRestricted = true; } status = (xhr.status === 1223) ? 204 : (xhr.status === 0 && ((self.location || {}).protocol == 'file:' || (self.location || {}).protocol == 'ionp:')) ? 200 : xhr.status; isCrossOriginRestricted = isCrossOriginRestricted || (status === 0); if (isCrossOriginRestricted ) { onError.call(Loader, "Failed loading synchronously via XHR: '" + url + "'; It's likely that the file is either " + "being loaded from a different domain or from the local file system whereby cross origin " + "requests are not allowed due to security reasons. Use asynchronous loading with " + "Ext.require instead.", synchronous); } else if ((status >= 200 && status < 300) || (status === 304) ) { // Debugger friendly, file names are still shown even though they're eval'ed code // Breakpoints work on both Firebug and Chrome's Web Inspector if (!Ext.isIE) { debugSourceURL = "\n//@ sourceURL=" + url; } Ext.globalEval(xhr.responseText + debugSourceURL); onLoad.call(scope); } else { onError.call(Loader, "Failed loading synchronously via XHR: '" + url + "'; please " + "verify that the file exists. " + "XHR status code: " + status, synchronous); } // Prevent potential IE memory leak xhr = null; } }
injectScriptElement方法,如下:
injectScriptElement: function(url, onLoad, onError, scope, charset) { var script = document.createElement('script'), dispatched = false, config = Loader.config, onLoadFn = function() { if(!dispatched) { // 注释6 dispatched = true; script.onload = script.onreadystatechange = script.onerror = null; if (typeof config.scriptChainDelay == 'number') { //free the stack (and defer the next script) defer(onLoad, config.scriptChainDelay, scope); } else { onLoad.call(scope); // 注释7 } Loader.cleanupScriptElement(script, config.preserveScripts === false, config.garbageCollect); } }, onErrorFn = function(arg) { defer(onError, 1, scope); //free the stack Loader.cleanupScriptElement(script, config.preserveScripts === false, config.garbageCollect); }; script.type = 'text/javascript'; script.onerror = onErrorFn; charset = charset || config.scriptCharset; if (charset) { script.charset = charset; } /* * IE9 Standards mode (and others) SHOULD follow the load event only * (Note: IE9 supports both onload AND readystatechange events) */ if ('addEventListener' in script ) { script.onload = onLoadFn; } else if ('readyState' in script) { // for <IE9 Compatability script.onreadystatechange = function() { if ( this.readyState == 'loaded' || this.readyState == 'complete' ) { onLoadFn(); } }; } else { script.onload = onLoadFn; } script.src = url; (Loader.documentHead || document.getElementsByTagName('head')[0]).appendChild(script); return script; }
onFileLoaded方法,如下:
onFileLoaded: function(className, filePath) { var loaded = isClassFileLoaded[className]; Loader.numLoadedFiles++; isClassFileLoaded[className] = true; // 标记类已经被加载 isFileLoaded[filePath] = true; // 标记对应路径的文件已经被加载 // In FF, when we sync load something that has had a script tag inserted, the load event may // sometimes fire even if we clean it up and set it to null, so check if we're already loaded here. if (!loaded) { Loader.numPendingFiles--; } if (Loader.numPendingFiles === 0) { Loader.refreshQueue(); } //<debug> if (!Loader.syncModeEnabled && Loader.numPendingFiles === 0 && Loader.isLoading && !Loader.hasFileLoadError) { var missingClasses = [], missingPaths = [], requires, i, ln, j, subLn; for (i = 0,ln = queue.length; i < ln; i++) { requires = queue[i].requires; for (j = 0,subLn = requires.length; j < subLn; j++) { if (isClassFileLoaded[requires[j]]) { missingClasses.push(requires[j]); } } } if (missingClasses.length < 1) { return; } missingClasses = Ext.Array.filter(Ext.Array.unique(missingClasses), function(item) { return !requiresMap.hasOwnProperty(item); }, Loader); if (missingClasses.length < 1) { return; } for (i = 0,ln = missingClasses.length; i < ln; i++) { missingPaths.push(classNameToFilePathMap[missingClasses[i]]); } throw new Error("The following classes are not declared even if their files have been " + "loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " + "corresponding files for possible typos: '" + missingPaths.join("', '")); } //</debug> }
require('soims.view.user.UserGrid',function(userGrid){
// 使用被加载并且被创建的userGrid实例
});
2. if(obj.hasOwnProperty(per)) 和 if(per in obj)有何区别?
3. pass(fn,[parm1,parm2])方法,返回可以接受额外参数的fn,详细见源码
onFileLoaded 是文件加载成功的回调函数,详见源码
4. 加载JS文件,详见源码
5. 向html中添加<script>标签,详见源码
6. 标示回调函数是否被执行过,为什么需要标示?难道回调函数会被触发多次吗?
7. 这里会调用注释3中,pass的返回函数
pass代码如下:
pass: function(fn, args, scope) { if (!Ext.isArray(args)) { if (Ext.isIterable(args)) { args = Ext.Array.clone(args); } else { args = args !== undefined ? [args] : []; } } return function() { var fnArgs = [].concat(args); fnArgs.push.apply(fnArgs, arguments); return fn.apply(scope || this, fnArgs); }; }
通过闭包,保存了类名和路径信息,当回调函数被执行的时候,又把这两个参数放到了回调函数的参数中,供回调函数使用。
这里的回调函数,如下:
function() { var fnArgs = [].concat(args); fnArgs.push.apply(fnArgs, arguments); return fn.apply(scope || this, fnArgs); }
简单的说:
在require()方法中,需要injectScriptElement()方法,并把onFileLoaded作为回调函数,传给injectScriptElement
但是在回调函数onFileLoaded中,需要require中的某个局部变量
所以,把变量和回调函数,暂时放到了闭包函数pass中,当回调函数被执行的时候,可以访问闭包中的变量
这里的pass()方法,是不是就是委托delegate?
小结,文件的异步加载的主要流程为:
浏览器加载文件 -> 加载完成就解析、执行文件 -> 执行Ext.define() -> 执行拦截器processors -> 执行Loader processor -> 抽取依赖关系dependencies -> 调用require()方法去逐个的加载文件 -> 逐个的向html中添加<script>标签 -> 浏览器加载文件
一旦某个文件加载、解析、执行完成,则执行回调函数onFileLoaded