Re-understand front-end AMD, CMD author: @tiffanysbear

This article is mainly aimed at some familiar front-end concepts before, when reviewing again, combined with their own development experience and use, to understand again. After development and online use, it will be even more impressive. Compare the RequireJS source code analysis to what needs to be considered when implementing a module loader.

The origin of

In fact, the difference between AMD and CMD, has been stuck in the use of different. There is no deep understanding of the difference, but it is mainly due to different performance characteristics and bottlenecks between the browser and Node.

Early JS modularization is mainly used in the browser side, the main demand and bottleneck lies in bandwidth, js needs to be downloaded from the server side, resulting in network performance overhead, so it is mainly to meet the needs of scope, on-demand loading. Hence the emergence of AMD (Asynchronous module definition), suitable for browser-side environments.

After the emergence of Node, the main performance cost is no longer network performance, disk read and write overhead can be ignored; CMD is conceptually more consistent with Node’s definition and understanding of CommonJS and can be loaded when needed. However, unlike the actual CommonJS, only reference-pointing relationships are generated when introduced.

Therefore, the two have different characteristics of use, and different phenomena occur when circular references occur. The following is an interpretation of the Requirejs source code. If you have any questions, please ask and correct them.

1, dynamic loading of a JS module method, how to ensure asynchronous and callback execution

First, we need to judge the environment, browser environment and Webworker environment. Create a script tag with document.createElement and use the async property to load javascript asynchronously. If IE is not compatible with async fields, use the load and onReadyStatechange event to execute the callback. The listening script is 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 propertyreturn node;
};
req.load = function(context, moduleName, url) {/ / used for js module loading method of var config = (context && context. The config) | | {}, node;if(isBrowser) {// Load js file node = req.createnode (config, moduleName, url); // Create a script tag node.setattribute ('data-requirecontext', context.contextName); / / requirecontext by default'_'
        node.setAttribute('data-requiremodule', moduleName); // Name of the current moduleif(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) {//script tag creation call 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 context.completeLoad(moduleName); } catch (e) {context.onerror (makeError()'importscripts'.'importScripts failed for ' +
                moduleName + ' at '+ url, e, [moduleName])); }}};Copy the code

2, how to judge to load JS, how to ensure the loading order

Use setTimeout to place it in the next queue to ensure the loading sequence

/ / bysetNextTick (context.nexttick (context.nexttick))function () {
	//Some defines could have been added since the
	//require call, collect them.
	intakeDefines();

	requireMod = getModule(makeModuleMap(null, relMap));

	//Store if map config should be applied to this require
	//call for dependencies.
	requireMod.skipMap = options.skipMap;

	requireMod.init(deps, callback, errback, {
		enabled: true
	});

	checkLoaded();
});
Copy the code

3. How can js files in require be judged to be loaded, and how can the number of loaded data be guaranteed to be correct?

The number of dependencies is calculated by depCount, iterating through the loop to count the number of dependencies. The following callback is not performed until the dependency depCount is reduced to 0.

// ...
enable: function () {
	enabledRegistry[this.map.id] = this;
	this.enabled = true;

	//Set flag mentioning that the module is enabling,
	//so that immediate calls to the defined callbacks
	//for dependencies do not trigger inadvertent load
	//with the depCount still being zero.
	this.enabling = true;

	//enableEach depends on each(this.depmaps,bind(this, function (depMap, i) {
		var id, mod, handler;

		if (typeof depMap === 'string') {
			//Dependency needs to be converted to a depMap
			//and wired up to this module.
			depMap = makeModuleMap(depMap,
				(this.map.isDefine ? this.map : this.map.parentMap),
				false,! this.skipMap); this.depMaps[i] = depMap; Handlers = getOwn(handlers, depmap.id);if (handler) {
				this.depExports[i] = handler(this);
				return; } this.depCount += 1; // dependency +1 on(depMap,'defined'.bind(this, function (depExports) {
				if (this.undefed) {
					return; } this.defineDep(i, depExports); // The loaded dependency module goes into depExports, passing this.check() to the function defined by require via apply; })); // Bind defined events while adding deP to Registryif (this.errback) {
				on(depMap, 'error'.bind(this, this.errback));
			} else if (this.events.error) {
				// No direct errback on this module, but something
				// else is listening for errors, so be sure to
				// propagate the error correctly.
				on(depMap, 'error'.bind(this, function (err) {
					this.emit('error', err); })); } } id = depMap.id; mod = registry[id]; // Skip some special modules such as:'require'.'exports'.'module'
		//Also, don't call enable if it is already enabled, //important in circular dependency cases. if (! hasProp(handlers, id) && mod && ! mod.enabled) { context.enable(depMap, this); // Load dependencies}})); //Enable each plugin that is used in //a dependency eachProp(this.pluginMaps, bind(this, function (pluginMap) { var mod = getOwn(registry, pluginMap.id); if (mod && ! mod.enabled) { context.enable(pluginMap, this); }})); this.enabling = false; this.check(); },Copy the code

The successful loading of a single file can be judged by polling checkLoaded every 50s, and the variable inCheckLoaded is used as a marker. Here is the checkLoaded function:

function checkLoaded() {
	var err, usingPathFallback,
		waitInterval = config.waitSeconds * 1000,
		//It is possible to disable the wait interval by using waitSeconds of 0.
		expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
		noLoads = [],
		reqCalls = [],
		stillLoading = false,
		needCycleCheck = true;

	//Do not bother if this call was a result of a cycle break.
	if (inCheckLoaded) {
		return;
	}

	inCheckLoaded = true;

	//Figure out the state of all the modules.
	eachProp(enabledRegistry, function (mod) {
		var map = mod.map,
			modId = map.id;

		//Skip things that are not enabled or in error state.
		if(! mod.enabled) {return;
		}

		if(! map.isDefine) { reqCalls.push(mod); }if(! mod.error) { //If the module should be executed, and it has not //been inited and time is up, remember it.if(! mod.inited && expired) {if (hasPathFallback(modId)) {
					usingPathFallback = true;
					stillLoading = true;
				} else{ noLoads.push(modId); removeScript(modId); }}else if(! mod.inited && mod.fetched && map.isDefine) { stillLoading =true;
				if(! map.prefix) { //No reason to keep lookingfor unfinished
					//loading. If the only stillLoading is a
					//plugin resource though, keep going,
					//because it may be that a plugin resource
					//is waiting on a non-plugin cycle.
					return (needCycleCheck = false); }}}});if (expired && noLoads.length) {
		//If wait time expired, throw error of unloaded modules.
		err = makeError('timeout'.'Load timeout for modules: ' + noLoads, null, noLoads);
		err.contextName = context.contextName;
		return onError(err);
	}

	//Not expired, check for a cycle.
	if (needCycleCheck) {
		each(reqCalls, function (mod) {
			breakCycle(mod, {}, {});
		});
	}

	//If still waiting on loads, and the waiting load is something
	//other than a plugin resource, or there are still outstanding
	//scripts, then just try back later.
	if((! expired || usingPathFallback) && stillLoading) { //Something is still waiting to load. Waitfor it, but only
		//if a timeout is not already in effect.
		if((isBrowser || isWebWorker) && ! checkLoadedTimeoutId) { checkLoadedTimeoutId =setTimeout(function() { checkLoadedTimeoutId = 0; checkLoaded(); }, 50); }}inCheckLoaded = false;
}
Copy the code

4, if there is a circular reference, how to judge, how to solve

I’m still a little confused about this part for the moment. Mark it first and then understand it.

See a breakCycle function that executes on the condition that needCycleCheck is true, but when! Mod.inited && mod.mod.touch&& map.isDefine module is not initialized, but after obtaining the definition and there is a prefix in map.prefix, breakCycle check will be started. As for why this is done, I can only guess that it is to break the polling query load state wait problem when the module require.

function breakCycle(mod, traced, processed) {
	var id = mod.map.id;

	if (mod.error) {
		mod.emit('error', mod.error);
	} else {
		traced[id] = true;
		each(mod.depMaps, function (depMap, i) {
			var depId = depMap.id,
				dep = getOwn(registry, depId);

			//Only force things that have not completed
			//being defined, so still in the registry,
			//and only if it has not been matched up
			//in the module already.
			if(dep && ! mod.depMatched[i] && ! processed[depId]) {if (getOwn(traced, depId)) {
					mod.defineDep(i, defined[depId]);
					mod.check(); //pass false?
				} else {
					breakCycle(dep, traced, processed); }}}); processed[id] =true; }}Copy the code

But in CommonJs, there are dependencies, because there are only references, code execution happens when the actual call is made, and there are variables at the beginning and end of the file to indicate whether the load is complete. Once a module has a cyclic dependency load, only the parts that have been executed are printed, and the parts that have not been executed are not printed.

So for AMD, CMD itself for the browser side, there is only a difference in the dependency declaration, itself will be the first to load the dependency, CMD so-called load on demand, in fact, just the difference in writing; It’s essentially the same as AMD. AMD is dependent on the front, CMD is dependent on the back, just in writing

In the case of cyclic loading of ES6 modules, ES6 is dynamically referenced, there is no cache value problem, and the module in which the variable binding is located; It doesn’t care if a looping load occurs, it just generates a reference to the loaded module, and it’s up to the developer to make sure it gets the value when it’s actually evaluated.