Requirejs is an implementation of the AMD (Asynchronous Module Definition- Asynchronous Module loading mechanism) specification that is worth looking at. To know the source code of RequireJS, we must first know how the module of RequireJS is defined, and know where the entry is, if we know how to call it, we will feel smooth when reading the source code.

While looking at the source code, I added some code comments. To view the annotated source code, I can fork it directly on my Github. The source code I have here is the latest version 2.3.5. The official Requirejs source code is also attached.

I’ve broken requirejs into three parts, with a closure and two defined global variables.

var requirejs, require, define;
(function (global, setTimeout) {
    // define some variables and tool methods
    var req, s, head ////some defined
    
    //add some function 
    
    // create a context for module loading
    function newContext(contextName) {
        //somecode
        
        // Define a module loader
        Module = function (map) {}
        Module.prototype = {
            // On the prototype chain
        };
        
        context = { // Context
            config: config, / / configuration
            contextName: contextName, // Default is "_"
            nextTick: req.nextTick, // Use setTimeout to put the execution on the next queue
            makeRequire: function (relMap, options) {
                
                function localRequire () {
                    //somecode
                    // Use setTimeout to load dependencies into the next queue to ensure the loading sequence
            		context.nextTick(function () {
            		
            			intakeDefines();
            
            			requireMod = getModule(makeModuleMap(null, relMap));
            			
            			requireMod.skipMap = options.skipMap;
            
            			requireMod.init(deps, callback, errback, {
            				enabled: true
            			});
            
            			checkLoaded();
            		});
            
            		return localRequire;
                }
                return localRequire;
            }
            //xxxx
		}
        context.require = context.makeRequire(); // Load time entry function
        return context;
    }
    
    //3, define require, define method, import data-main path and module load
    req = requirejs = function (deps, callback, errback, optional) {
        //xxxx
        
        context = getOwn(contexts, contextName);  // Get the default environment
    	if(! context) { context = contexts[contextName] = req.s.newContext(contextName);// Create an environment name named '_'
    	}
    
    	if (config) {
    		context.configure(config);  // Set the configuration
    	}
    
    	return context.require(deps, callback, errback);
    }
    
    req.config = function (config) {
		return req(config);
	};
	
	s = req.s = {
		contexts: contexts,
		newContext: newContext
	};
	
    req({}); // Initialize the context in which the module is loaded
    
    define = function (name, deps, callback) {
    
    }
    
    req(cfg); // Load data-main, the main entry js} (this, (typeof setTimeout === 'undefined' ? undefined : setTimeout)));
Copy the code

The above code basically shows the three requireJS parts, with a lot of code omitted. After looking at the outline structure, follow me step by step to see how RequireJS loads and defines modules.

How does requirejs load entry JS

Those of you who have used Requirejs know that when we introduce it, we add the data-main property to the script tag as an entry point for configuration and module loading. The specific code is as follows:

<script type="text/javascript" src="./require.js" data-main="./js/main.js"></script>
Copy the code

Requirejs first determines whether the current is a browser environment. If so, it traverses all script tags on the current page, takes out the data-main attribute, and obtains baseUrl and the file name of js to be loaded in advance through calculation. The specific code is as follows:

varisBrowser = !! (typeof window! = ='undefined' && typeofnavigator ! = ='undefined' && window.document);

function scripts() { // Get all the target tags on the page
    return document.getElementsByTagName('script');
}

function eachReverse(ary, func) {
    if (ary) {
        var i;
        for (i = ary.length - 1; i > - 1; i -= 1) {
            if (ary[i] && func(ary[i], i, ary)) {
                break; }}}}if (isBrowser) {
    head = s.head = document.getElementsByTagName('head') [0];
    baseElement = document.getElementsByTagName('base') [0];
    if(baseElement) { head = s.head = baseElement.parentNode; }}if(isBrowser && ! cfg.skipDataMain) { eachReverse(scripts(),function (script) {  // Iterate over all script tags
    	// If the head tag does not exist, make the parent node of the script tag act as the head
    	if(! head) { head = script.parentNode; } dataMain = script.getAttribute('data-main');
    	if (dataMain) {  // Get the data-main attribute (if present)
    		// Save the dataMain variable in case there is a path (i.e. contains '? ')
    		mainScript = dataMain;
    		
    		// If no explicit baseUrl is specified, set the path of data-main to baseUrl
    		// Do this only if the value of data-main is not the module ID of a plug-in
    		if(! cfg.baseUrl && mainScript.indexOf('! ') = = =- 1) {
    			// Fetch the path in data-main as baseUrl
    			src = mainScript.split('/'); // Use the/to cut the path
    			mainScript = src.pop();  // Take out the js name in data-main
    			subPath = src.length ? src.join('/') + '/' : '/';  // Concatenate the parent path. If data-main has only one path, it indicates the current directory
    			cfg.baseUrl = subPath;
    		}
    		
    		// remove the js suffix for the module name
    		mainScript = mainScript.replace(jsSuffixRegExp, ' ');
    		// If mainScript is still a path, reset mainScript to dataMain
    		
    		if (req.jsExtRegExp.test(mainScript)) {
    			mainScript = dataMain;
    		}
    		
    		// Put the module name of data-main into the deps array
    		cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
    
    		return true; }}); }Copy the code

After doing this, we get a CFG object with two properties baseUrl and deps. For example, in our case, the script tag has an attribute data-main=”./js/main.js”. Requirejs converts the required CFG object to:

cfg = {
    baseUrl: "./js/".deps: ["main"]}Copy the code

Requirejs calls the req method: req(CFG); . The req method is the require method, which is the entry function of the entire Requirejs. It acts as a distributor to match parameter types and determine whether the current operation is config or require. A context is created in this method. All module loading and require related configuration takes place in this context. The call to the req (CFG); Previously, requirejs also called the req method once: req({}); , this step is to create the context for the module to load. Let’s take a look at the source of the req method directly:

// The variable originally defined
var defContextName = '_'.// The name of the module loaded by default
    contexts = {}; // The container of the context in which the module is loaded

req = requirejs = function (deps, callback, errback, optional) {
    //Find the right context, use default
    var context, config,
    	contextName = defContextName; // The default context
    // Parameter correction
    // Determine if have config object in the call.
    if(! isArray(deps) &&typeofdeps ! = ='string') {
    	// deps is a config object
    	config = deps;  // The first parameter is represented as a configuration parameter if it is not an array or a string
    	if (isArray(callback)) {
    		// Callback is deps
    		deps = callback;
    		callback = errback;
    		errback = optional;
    	} else{ deps = []; }}if (config && config.context) {
    	contextName = config.context;
    }
    
    context = getOwn(contexts, contextName);  // Get the default environment
    if(! context) {// If it is entered for the first time, call the newContext method to create
    	context = contexts[contextName] = req.s.newContext(contextName); // Create an environment name named '_'
    }
    
    if (config) {
    	context.configure(config);  // Set the configuration
    }
    
    // If the deps, callback, and errback parameters are empty, then nothing happens when you call require
    return context.require(deps, callback, errback); // Call the require method in the context to load the module
};

req.config = function (config) {
    return req(config); The //require.config method also ends up calling req
};

if (!require) {  // the require method is req
    require = req;
}

s = req.s = {
    contexts: contexts,
    newContext: newContext // Create a new context
};
Copy the code

Continue as before req(CFG); Context. configure(config) is called based on the CFG passed in; The context is created by the newContext function of the second requirejs part. The resulting context will be stored in the global Context object. We can print the Contexts object in the console and see that there is only one context named ‘_’ in it, which is the context specified by Requrejs by default.

The newContext function has a number of local variables to cache loaded modules and a Module loader, which will be used later. Let’s start with the configure method called:

function newContext (contextName) {
    var context, config = {};
    
    context = {
        configure: function (cfg) {
            // Make sure baseUrl ends with a /
            if (cfg.baseUrl) { 
            	// The root path of all modules,
            	// Path of the default requirejs file,
            	// If data-main is set, it is the same as data-main
            	if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1)! = ='/') {
            		cfg.baseUrl += '/'; }}// Other code to add some alternative configuration, not related to this load
            
            // If deps or callback is specified in the configuration item, require is called
            It is useful to use require definition objects as configuration before requirejs is loaded
            if(cfg.deps || cfg.callback) { context.require(cfg.deps || [], cfg.callback); }},makeRequire: function (relMap, options) {}}return context;
} 
Copy the code

CFG: context.require(cfg.deps); CFG: requirejs: context.require(cfg.deps); . As you can see from the above code, the context require method is created using makeRequire. The reason why makeRequire is used to create the require method is to create a function scope, which is convenient to extend some properties of the require method.

context = {
    makeRequire: function (relMap, options) {
        options = options || {};
        function localRequire(deps, callback, errback) { // The real require method
        	var id, map, requireMod;
        
        	if (options.enableBuildCallback && callback && isFunction(callback)) {
        		callback.__requireJsBuild = true;
        	}
        
        	if (typeof deps === 'string') {
        		// If deps is a string instead of an array, do something else
        	}
        	
        	intakeDefines();
        
        	// Use setTimeout to load dependencies into the next queue to ensure the loading sequence
        	context.nextTick(function () {
        		intakeDefines();
        
        		requireMod = getModule(makeModuleMap(null, relMap));
        		
        		requireMod.skipMap = options.skipMap;
        
        		requireMod.init(deps, callback, errback, {
        			enabled: true
        		});
        
        		checkLoaded();
        	});
        
        	return localRequire;
        }
        // The mixin type and extend methods extend the properties of an object
        mixin(localRequire, {
            isBrowser,
            isUrl,
            defined,
            specified
        });
        
        returnlocalRequire; }}; context.require = context.makeRequire();// Load time entry function
Copy the code

I started reading the source code using the break point method, and every time I saw context.nexttick, I stopped working on it and was puzzled. Then I looked at what nextTick was for and realized it was actually a timer.

context = {
    nextTick: req.nextTick, // Use setTimeout to put the execution on the next queue
};
req.nextTick = typeofsetTimeout ! = ='undefined' ? function (fn) {
	setTimeout(fn, 4);
} : function (fn) { fn(); };
Copy the code

I also don’t understand why you put some master logic into a timer so that all the loading goes to the next task queue. A look at the requireJS version iteration shows that nextTick was added in version 2.10, and there was no such logic before. And even if I removed the nextTick logic from the RequireJS source code, the code worked fine.

Tips: setTimeout is set to 4ms because the minimum delay of setTimeout (DOM_MIN_TIMEOUT_VALUE) specified in the HTML5 specification is 4ms. However, after 2010, all browser implementations follow this rule, which is 10ms prior to 2010.

Later, I referred to some ideas of other blogs on the network. Some people think that setTimeout is set to load modules in order to make modules load in order. I have not studied this thoroughly, so I set a todo here, hahaha.

Finally, I found the documentation on the Requirejs wiki. Officially, the purpose is to make the loading of modules asynchronous, in order to prevent minor bugs (exactly what bugs are not clear).

Anyway, here’s the source code for Requirejs. In nextTick, a module map is first constructed using makeModuleMap, and then a new module loader is immediately created using getModule.

//requireMod = getModule(makeModuleMap(null, relMap)); // Code in nextTick

// Create a module map
function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
    var url, pluginModule, suffix, nameParts,
    	prefix = null,
    	parentName = parentModuleMap ? parentModuleMap.name : null,
    	originalName = name,
    	isDefine = true.// Is the module of define
    	normalizedName = ' ';
    
    // If there is no module name, require is called, using an internal name
    if(! name) { isDefine =false;
    	name = '_@r' + (requireCounter += 1);
    }
    
    nameParts = splitPrefix(name);
    prefix = nameParts[0];
    name = nameParts[1];
    
    if (prefix) { // If there is a plug-in prefix
    	prefix = normalize(prefix, parentName, applyMap);
    	pluginModule = getOwn(defined, prefix); // Get the plug-in
    }
    
    if (name) {
        // Do some more special processing for name
    }
    
    return {
        prefix: prefix,
        name: normalizedName,
        parentMap: parentModuleMap,
        unnormalized:!!!!! suffix,url: url,
        originalName: originalName,
        isDefine: isDefine,
        id: (prefix ?
            prefix + '! ' + normalizedName :
            normalizedName) + suffix
    };
}

// Get a module loader
function getModule(depMap) {
    var id = depMap.id,
        mod = getOwn(registry, id);
    
    if(! mod) {// Add unregistered modules to the module registry
        mod = registry[id] = new context.Module(depMap);
    }
    
    return mod;
}

// Module loader
Module = function (map) {
    this.events = getOwn(undefEvents, map.id) || {};
    this.map = map;
    this.shim = getOwn(config.shim, map.id);
    this.depExports = [];
    this.depMaps = [];
    this.depMatched = [];
    this.pluginMaps = {};
    this.depCount = 0;
    
    /* this.exports this.factory this.depMaps = [], this.enabled, this.fetched */
};

Module.prototype = {
    init: function () {},
    fetch: function () {},
    load: function () {},
    callPlugin: function () {},
    defineDep: function () {},
    check: function () {},
    enable: function () {},
    on: function () {},
    emit: function () {}};Copy the code
requireMod.init(deps, callback, errback, {
	enabled: true
});
Copy the code

As soon as you get the created module loader, you call the init method. Init calls enable, which creates a new module loader for all depMaps and calls the enable method of the dependency module loader, and finally calls check, which immediately calls fetch. Fatch finally calls the load method, which quickly calls context.load. A picture is better than a thousand words.

It is true that the logic of this piece is very convoluted. In the middle, each method has some modifications to some parameters in the scope. Focus here on the req.load method, which is how all modules are loaded.

req.createNode = function (config, moduleName, url) {
    var node = config.xhtml ?
        document.createElementNS('http://www.w3.org/1999/xhtml'.'html:script') :
        document.createElement('script');
    node.type = config.scriptType || 'text/javascript';
    node.charset = 'utf-8';
    node.async = true; // Create script tag to add async property
    return node;
};
req.load = function (context, moduleName, url) { // The method used to load js modules
    var config = (context && context.config) || {},
    	node;
    if (isBrowser) { // Load the js file in the browser
    
        node = req.createNode(config, moduleName, url); // Create a script tag
        
        node.setAttribute('data-requirecontext', context.contextName); // requireContext defaults to '_'
        node.setAttribute('data-requiremodule', moduleName); // Name of the current module
        
        if(node.attachEvent && ! (node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
            !isOpera) {
            
            useInteractive = true;
            
            node.attachEvent('onreadystatechange', context.onScriptLoad);
        } else {
            node.addEventListener('load', context.onScriptLoad, false);
            node.addEventListener('error', context.onScriptError, false);
        }
        node.src = url;
        
        if (config.onNodeCreated) { // The callback when the script tag is created
            config.onNodeCreated(node, config, moduleName, url);
        }
        
        currentlyAddingScript = node;
        if (baseElement) { // Add the script tag to the page
            head.insertBefore(node, baseElement);
        } else {
            head.appendChild(node);
        }
        currentlyAddingScript = null;
        
        return node;
    } else if (isWebWorker) { // In the webWorker environment
    	try {
            setTimeout(function () {},0);
            importScripts(url); //webWorker uses importScripts to load scripts
            
            context.completeLoad(moduleName);
    	} catch (e) { // Load failed
            context.onError(makeError('importscripts'.'importScripts failed for ' +
                moduleName + ' at '+ url, e, [moduleName])); }}};Copy the code

Requirejs loads modules by creating script tags and inserting the created script tags into the head. It is also supported in Webwork, where importScripts() is used to load modules in webWorker.

Finally, you can see that there is a script in the head tag:

Define a module using define

Requirejs provides a module definition method: define, which follows the AMD specification and is used as follows:

define(id? , dependencies? , factory);Copy the code

The meanings of the define three parameters are as follows:

  1. Id indicates the module name and can be ignored. If omitted, anonymous modules are defined.
  2. Dependencies are an array of module dependencies.
  3. Factory is a module definition function. The return value of this function is to define the module. If it has dependencies, the parameters of this function are each item in the array.

Factory also supports commonJS to define modules. If define does not pass in an array of dependencies, factory defaults to require, exports, and module. Yes, these three parameters are consistent with how commonJS is loaded. Require is used to import modules, exports and modules are used to export modules.

// write method 1:
define(
    ['dep1'].function(dep1){
        var mod;
        / /...
        
        returnmod; });// write method 2:
define(
    function (require, exports, module) {
        var dep1 = require('dep1'), mod;

        / /...exports = mod; }});Copy the code

Nonsense not to say, we still look at the source code directly!

/** * is used to define module functions. Unlike the require method, where the module name must be the first argument and be a string, the * module-defined function (callback) must return a value corresponding to the module name represented by the first argument */
define = function (name, deps, callback) {
	var node, context;

	// Run the anonymous module
	if (typeofname ! = ='string') {
		// Parameter matching
		callback = deps;
		deps = name;
		name = null;
	}

	// This module can have no dependencies
	if(! isArray(deps)) { callback = deps; deps =null;
	}

	// If no name is specified and callback is a function, use the commonJS form to introduce the dependency
	if(! deps && isFunction(callback)) { deps = [];// Remove the comment from callback,
		// Pull require out of callback and push the dependency into the DEps array.
		// Do this only if the argument passed in callback is not null
		if (callback.length) { // Convert the module's callback function to a string and do some processing
			callback
				.toString()
				.replace(commentRegExp, commentReplace) // remove comments
				.replace(cjsRequireRegExp, function (match, dep) {
					deps.push(dep); // match all modules that call require
				});

			// Compatible with CommonJS
			deps = (callback.length === 1 ? ['require'] : ['require'.'exports'.'module']).concat(deps); }}//If in IE 6-8 and hit an anonymous define() call, do the interactive
	//work.
	if (useInteractive) { // Ie 6-8 for special processing
		node = currentlyAddingScript || getInteractiveScript();
		if (node) {
			if(! name) { name = node.getAttribute('data-requiremodule');
			}
			context = contexts[node.getAttribute('data-requirecontext')]; }}// If the context exists, the modules are placed in the context's defQueue, and the defined modules are placed in the global dependency queue without contenxt
	if (context) {
		context.defQueue.push([name, deps, callback]);
		context.defQueueMap[name] = true;
	} else{ globalDefQueue.push([name, deps, callback]); }};Copy the code

All modules defined by define will end up in the globalDefQueue array, the defQueue array of the current context. How to get the modules defined is done using takeGlobalQueue.


/** * Internal method that takes the globalQueue dependencies and places them in the current context */
function intakeDefines() { // Get and load the module added by the define method
	var args;

	// Fetch all modules defined by the define method (in globalQueue)
	takeGlobalQueue();

	//Make sure any remaining defQueue items get properly processed.
	while (defQueue.length) {
		args = defQueue.shift();
		if (args[0= = =null) {
			return onError(makeError('mismatch'.'Mismatched anonymous define() module: ' +
				args[args.length - 1]));
		} else {
			//args are id, deps, factory. Should be normalized by the
			//define() function.
			callGetModule(args);
		}
	}
	context.defQueueMap = {};
}

function takeGlobalQueue() {
	// Add the global DefQueue to the current context DefQueue
	if (globalDefQueue.length) {
		each(globalDefQueue, function (queueItem) {
			var id = queueItem[0];
			if (typeof id === 'string') {
				context.defQueueMap[id] = true; } defQueue.push(queueItem); }); globalDefQueue = []; }}// The intakeDefines() method is invoked in makeRequire
makeRequire: function (relMap, options) { // used to construct the require method
    options = options || {};
    
    function localRequire(deps, callback, errback) { // The real require method
    
        intakeDefines();
        
        context.nextTick(function () {
			//Some defines could have been added since the
			//require call, collect them.intakeDefines(); }}}// The takeGlobalQueue method is also called when the dependency is loaded
// We mentioned earlier that Requirejs loads modules by inserting a script tag into the head header
// When the module is loaded, a load event is bound to the script tag
node.addEventListener('load', context.onScriptLoad, false);

// This event ends up calling the completeLoad method
onScriptLoad: function (evt) {
	if (evt.type === 'load' ||
		(readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
		var data = getScriptData(evt);
		context.completeLoad(data.id);
	}
}

completeLoad: function (moduleName) {
    var found;
    takeGlobalQueue();// Get the module defined in the loaded JS
	while (defQueue.length) {
		args = defQueue.shift();
		if (args[0= = =null) {
			args[0] = moduleName;
			
			if (found) {
				break;
			}
			found = true;
		} else if (args[0] === moduleName) {
			found = true;
		}

		callGetModule(args);
	}
	context.defQueueMap = {};
}

Copy the code

Either get the module defined by Defie via require or get the module defined by scriptLoad after the dependency is loaded, callGetModule() is used for module loading in both methods. Let’s take a closer look at what happens after callGetModule.

function callGetModule(args) {
	// Skip loaded modules
	if(! hasProp(defined, args[0])) {
		getModule(makeModuleMap(args[0].null.true)).init(args[1], args[2]); }}Copy the code

The callGetModule method actually calls the getModule method (described earlier), which returns an instance of Module (Module loader), and then calls its init method. Init calls check, which executes the factory defined by define, and saves the module name and module in the defined global variable.

exports = context.execCb(id, factory, depExports, exports);
defined[id] = exports;
Copy the code

This is the end of defining modules. This article is written here first, which only clarifies the module definition, the initial loading of RequireJS and how js is introduced as the entrance of RequireJS. Many details are not covered in this part. Here, the next section will delve into the composition of the Module loader and how the require method introduces dependencies.

See you next time.

The original link