從新理解前端 AMD、CMD

從新理解前端 AMD、CMD
author: @TiffanysBearjavascript

本文主要是針對以前一些熟悉的前端概念,再次回顧的時候,結合本身的開發經驗和使用,進行再次理解。通過了開發和線上使用以後,會有更爲深入的印象。對比requirejs源碼分析,實現一個模塊加載器,須要考慮哪些問題。html

起源

其實對於AMD和CMD的不一樣,以前一直是拘泥在使用上的不一樣。沒有深入的認識爲何會有不一樣,其實主要是由於瀏覽器端和 Node 端不一樣性能特色和瓶頸帶來的不一樣。前端

早期的js模塊化主要用於瀏覽器端,主要的需求和瓶頸在於帶寬,須要將js從服務端下載下來,從而帶來的網絡性能開銷,所以主要是知足對於做用域、按需加載的需求。所以AMD(異步模塊定義)的出現,適合瀏覽器端環境。java

然後出現Node以後,主要的性能開銷再也不是網絡性能,磁盤的讀寫和開銷能夠忽略不計;CMD在理念上更符合Node對於CommonJS的定義和理解,在須要時進行加載;可是和實際的CommonJS有區別,引入時只是產生引用指向關係。node

所以二者產生了不一樣的使用特色,在出現循環引用時,就產生了不一樣的現象。如下是針對 requirejs 源碼部分的解讀。若是有問題,歡迎提問糾正。git

一、動態加載一個js模塊的方法,怎麼保證異步和回調的執行

一先開始是須要判斷環境,瀏覽器環境和webworker環境;若是是瀏覽器環境,經過document.createElement 建立script標籤,使用async屬性使js能進行異步加載, IE等不兼容async字段的,經過監聽 load 、 onreadystatechange 事件執行回調,監聽腳本加載完成。github

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; //建立script標籤添加了async屬性
    return node;
};
req.load = function (context, moduleName, url) { //用來進行js模塊加載的方法
    var config = (context && context.config) || {},
    	node;
    if (isBrowser) { //在瀏覽器中加載js文件
    
        node = req.createNode(config, moduleName, url); //建立一個script標籤
        
        node.setAttribute('data-requirecontext', context.contextName); //requirecontext默認爲'_'
        node.setAttribute('data-requiremodule', moduleName); //當前模塊名
        
        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) { //script標籤建立時的回調
            config.onNodeCreated(node, config, moduleName, url);
        }
        
        currentlyAddingScript = node;
        if (baseElement) { //將script標籤添加到頁面中
            head.insertBefore(node, baseElement);
        } else {
            head.appendChild(node);
        }
        currentlyAddingScript = null;
        
        return node;
    } else if (isWebWorker) { //在webWorker環境中
    	try {
            setTimeout(function () { }, 0);
            importScripts(url); //webWorker中使用importScripts來加載腳本
            
            context.completeLoad(moduleName);
    	} catch (e) { //加載失敗
            context.onError(makeError('importscripts',
                'importScripts failed for ' +
                moduleName + ' at ' + url,
                e,
                [moduleName]));
    	}
    }
};


複製代碼

二、怎麼判斷去加載js,怎麼保證加載的順序

經過 setTimeout 放入下一個隊列中,保證加載順序web

//經過setTimeout的方式加載依賴,放入下一個隊列,保證加載順序
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();
});
複製代碼

三、require中的js文件是怎麼判斷已經loaded,怎麼保證加載數據的數量是正確的?

依賴數量,是經過 depCount 來計算的,經過循環遍歷,統計具體的依賴數量;直到依賴depCount減小到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;

	//enable每個依賴
	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; //獲取的依賴映射

			handler = getOwn(handlers, depMap.id);

			if (handler) {
				this.depExports[i] = handler(this);
				return;
			}

			this.depCount += 1; //依賴項+1

			on(depMap, 'defined', bind(this, function (depExports) {
				if (this.undefed) {
					return;
				}
				this.defineDep(i, depExports); //加載完畢的依賴模塊放入depExports中,經過apply方式傳入require定義的函數中
				this.check();
			})); //綁定defined事件,同時將dep添加到registry中

			if (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];

		//跳過一些特殊模塊,好比:'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); //加載依賴 } })); //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(); }, 複製代碼

判斷單個文件加載成功,是經過 checkLoaded 每間隔 50s 作一次輪詢進行判斷,變量 inCheckLoaded 做爲標識;下面是 checkLoaded 函數:緩存

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 looking for 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. Wait for it, but only
		//if a timeout is not already in effect.
		if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
			checkLoadedTimeoutId = setTimeout(function () {
				checkLoadedTimeoutId = 0;
				checkLoaded();
			}, 50);
		}
	}

	inCheckLoaded = false;
}
複製代碼

四、若是有循環引用,怎麼判斷出的,怎麼解決的

這部分暫且還有點疑惑,先mark一下,以後再理解;

看到有個 breakCycle 函數,執行條件是 needCycleCheck 爲 true,可是當 !mod.inited && mod.fetched && map.isDefine 模塊未被初始化完成,可是已經獲取過定義過以後,且 在 map.prefix 有前綴,會啓動 breakCycle 檢查;至於爲何要這麼作,只能猜想是爲了到模塊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;
	}
}
複製代碼

可是在CommonJs中時,存在依賴的狀況下,由於存在的只是引用,代碼執行是在實際調用時才發生,在文件的開頭和結尾也會有變量標識是否加載完成。一旦某個模塊出現循環依賴加載,就只輸出已經執行到的部分,還未執行的部分不會輸出。

因此對於AMD、CMD自己對於瀏覽器端而言,存在的只是依賴聲明的不一樣,自己各自都會先去加載依賴,CMD所謂的按需加載,其實只是寫法上的區別;本質上和AMD並沒有區別。AMD是依賴前置、CMD是依賴後置,只是在寫法上

在ES6模塊加載的循環加載狀況下,ES6是動態引用的,不存在緩存值問題,並且模塊裏面的變量綁定所在的模塊;不關心是否發生了循環加載,只是生成一個指向被加載模塊的引用,須要開發者本身來保證真正取值的時候可以取到值。

相關文章
相關標籤/搜索