blog
blog copied to clipboard
AMD加载器分析与实现
什么是AMD(不是做显卡的:joy:)?如果不熟的话,require.js总应该比较熟。
AMD是_Asynchronous Module Definition_的缩写,字面上即异步模块定义。require.js是模块加载器,实现了AMD的规范。
本文想说的就是怎么实现一个类似require.js的加载器。但在这之前,我们应该了解下JS模块化的历史。
https://github.com/Huxpro/js-module-7day
这个Slides讲的比我好的多,所以想了解前端模块化的前世今生的可以去看看。这里简单总结下:
为什么需要模块化?
- Web Pages正在变成 Web App,应用越大那么代码也越复杂;
- 模块化利于解耦,降低复杂性和提高可维护性;
- 应用部署可以优化代码,减少http请求(避免多模块文件造成的多请求)。
前端模块历史?
- 无模块,全局作用域冲突;
- namespace封装,减少暴露给全局作用域的变量,本质是对象,不安全;
- IIFE;
- 添加依赖的IIFE,即模块化,也是现代模块化的基础;
但模块化还需要解决加载问题:
- 原始的script tag,有难以维护,依赖模糊,请求过多的问题;
- script loader,如Lab.js,基于文件的依赖管理;
- module loader,YUI;
- CommonJS,node提供的模块化和加载方案,由于是同步/阻塞加载,所以只适合服务器/本地;
- AMD/CMD,异步加载;
- Browserify/Webpack,去掉
define包裹,在打包时解决模块化; - ES6带来语言原生的模块化方案。
好ï¼ä¸é¢å¤§æ¦èå®äºæ¨¡ååçèæ¯ï¼é¡ºä¾¿å®å©äº_模åå䏿¥è°_ï¼åçççå¾å¥½ï¼ï¼ä¸é¢æ¥å ¥æ£é¢ï¼æä¹å®ç°ä¸ä¸ªAMD Loaderï¼
读读Amdçè§èï¼ç»åæä»¬ä½¿ç¨require.jsçç»éªï¼å
¶å®æ ¸å¿å°±æ¯è¦å®ç°defineï¼requireä¸¤ä¸ªå½æ°ã
å½ç¶å¨è¿ä¹åï¼æä»¬å 设å®ä¸ä¸ç®æ ï¼æè è¯´ææ¸ä¸ä¸ª_Amd loader_çèæ¯ã
çè§£_Amd loader_çåçï¼è®©æ°æå»é¤å¯¹require.jsçloaderçç¥ç§æï¼ç解模ååè¿ä½æºå¶ãè¿æ¯æåè¿ç¯æç« çç®çã
å¨åè¿ç¯æç« çè¿ç¨ä¸ï¼æé
读äºä¸äºç¸å
³æç« ï¼çäºrequire.jsçæäºå®ç°ï¼è¿äºé½å¯¹æ¬æææå¸®å©ï¼é常æè°¢ðã
1. åå¤å·¥ä½
é¦å æä¸äºå·¥å ·å½æ°ï¼ä¸äºå置工ä½å æåºæ¥è®²ã
1.1 æä¹å 载模å/æä»¶ï¼
éè¿scriptæ ç¾ãè¿æ¯æç®åèªç¶çæ¹æ³ï¼å ¶å®å¯ä»¥ajaxå è½½æºä»£ç evalï¼å©ç¨workerççã
/**
* load script
* @param {String} url script path
* @param {Function} callback function called after loaded
*/
function loadScript(url, callback) {
var node = document.createElement('script');
var supportOnload = 'onload' in node;
node.charset = CONFIG.charset || 'utf-8';
node.setAttribute('data-module', url);
// bind events
if (supportOnload) {
node.onload = function() {
onload();
};
node.onerror = function() {
onload(true);
};
} else {
// https://github.com/requirejs/requirejs/blob/master/require.js#L1925-L1935
node.onreadystatechange = function() {
if (/loaded|complete/.test(node.readyState)) {
onload();
}
}
}
node.async = true;
node.src = url;
// 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;
// ref: #185 & http://dev.jquery.com/ticket/2709
baseElement ? head.insertBefore(node, baseElement) : head.appendChild(node);
currentlyAddingScript = null;
function onload(error) {
// ensure only execute once
node.onload = node.onerror = node.onreadystatechange = null;
// remove node
head.removeChild(node);
node = null;
callback(error);
}
}
ä»£ç æ²¡ä»ä¹å¤æé»è¾ï¼å¾å¥½çè§£ï¼å°±æ¯å建<script>æ ç¾å è½½èæ¬ï¼å®æåå 餿 ç¾ï¼è°ç¨åè°ã
ç¨å¾®éè¦æ³¨æçæ¯è¿é设置äºcurrentlyAddingScriptï¼ç¨äºææå½åå è½½æ§è¡çæ¯åªä¸ªèæ¬ã
1.2. module idçå½åè§åï¼idåurlç转æ¢è§å
define(id, deps, factory)å®ä¹æ¨¡å
对äºdefineèè¨ï¼id妿åºç°ï¼å¿
é¡»æ¯â顶级âçåç»å¯¹çï¼ä¸å
许ç¸å¯¹ååï¼ãæ¯å¦jqueryæ¯åæ³çï¼./jqueryæ¯éæ³çã
depsé乿¯idï¼ä½årequire(deps, callback)ä¸depsæ
å½¢ä¸è´ï¼æä»¥æ¾å°ä¸é¢è®²ã
ä¾èµæ¨¡åid
ä¾èµæ¨¡åæ°ç»ä¸çidæä»¥ä¸å 个æ å½¢ï¼
- idæ¯ç»å¯¹è·¯å¾ï¼å¦
require(['/lib/util', 'http://cdn.com/lib.js'], callback)ï¼ - idæ¯ç¸å¯¹è·¯å¾ï¼å¦
require(['./lib/util', '../a/b'], callback)ï¼ - idæ¯IDåï¼å¦
jqueryã
对äº1å2èè¨ï¼id齿¯urlï¼å¯ä»¥ç»ä¸å¤çã对äº2ï¼ä»¥å½å模åæå¨çç®å½æ¥æç¸å¯¹è·¯å¾è½¬åæç»å¯¹è·¯å¾ã对äºç»å¯¹è·¯å¾ï¼é£ä¹æ¤æ¶æ¯åæ³çidï¼æ¤æ¶idåurlç¸çã
对äº3èè¨ï¼ä¸è¬éè¦è®¾ç½®config.pathsï¼å 为ä»
ä»
æ ¹æ®è¿ä¸ªidæ æ³è·åurlï¼å³æ æ³å è½½ã
var dotReg = /\/\.\//g;
var doubleDotReg = /\/[^/]+\/\.\.\//;
var multiSlashReg = /([^:/])\/+\//g;
var ignorePartReg = /[?#].*$/;
var suffixReg = /\.js$/;
var dirnameReg = /[^?#]*\//;
function fixPath(path) {
// /a/b/./c/./d --> /a/b/c/d
path = path.replace(dotReg, "/");
// a//b/c --> a/b/c
// a///b////c --> a/b/c
path = path.replace(multiSlashReg, "$1/");
// a/b/c/../../d --> a/b/../d --> a/d
while (path.match(doubleDotReg)) {
path = path.replace(doubleDotReg, "/");
}
// main/test?foo#bar --> main/test
path = path.replace(ignorePartReg, '');
if (!suffixReg.test(path)) {
path += '.js';
}
return path;
}
function dirname(path) {
var m = path.match(dirnameReg);
return m ? m[0] : "./";
}
function id2Url(url, baseUrl) {
url = fixPath(url);
if (baseUrl) {
url = fixPath(dirname(baseUrl) + url);
}
if (CONFIG.urlArgs) {
url += CONFIG.urlArgs;
}
return url;
}
2. æ´ä½è®¾è®¡
é¦å
æä»¬å¿
ç¶è¦æ´é²defineï¼require彿°ç»å
¨å±å¯¹è±¡ï¼å è½½çæ¨¡åä¹åºè¯¥ç¼åï¼é£ä¹loaderçåºæ¬ç»æåºè¯¥å¦ä¸ï¼
(function(root) {
var CONFIG = {
baseUrl: '',
charset: '',
paths: {},
shim: {}
};
var MODULES = {};
var cache = {
modules: MODULES,
config: CONFIG
};
...
var define = function(id, deps, factory) {};
define.amd = {};
var require = function(ids, callback) {};
require.config = function(config) {};
// export to root
root.define = define;
root.require = require;
})(this);
ç¶å设计æä»¬ç模åç³»ç»ã以é¢å对象çæç»´ï¼ææ¯ä¸ªæ¨¡åæ½è±¡æModuleç±»çå®ä¾ï¼
- 彿们éè¦è·åä¸ä¸ªæ¨¡åæ¶ï¼é¦å å°è¯ä»ç¼å䏿¥æ¾ï¼æ²¡æå以urlådepsï¼å¯éï¼å建ä¸ä¸ªæ¨¡åå®ä¾ã
- 模åå¼å§åå§åã
- 模åædepsè·åèªå·±çææä¾èµæ¨¡åï¼è·åæ¹å¼æç¬¬ä¸æ¥å¼å§ã
- 模åæèªå·±æ·»å å°depsä¸å个ä¾èµçå¼ç¨æ¨¡åå表ä¸ã
- 妿ææçä¾èµæ¨¡åå è½½å®æ¯ï¼å模åèªèº«è¿è¡factoryï¼è®¾ç½®exportsï¼æ å¿èªå·±å è½½å®æï¼å¹¶notifyèªå·±çå¼ç¨æ¨¡åå表ã
è®¾è®¡ä¸ææ ¸å¿çä¸ç¹æ¯åæ²»ææ³ãæä»¬ç¥éï¼è¦æä¸ä¸ªæ¨¡åAçæ£å è½½å®æï¼å¿ 须确认å®çææä¾èµæ¨¡åå è½½å®æãç¶å模åAæ¬èº«ä¹å¯ä»¥ä½ä¸ºå ¶å®æ¨¡åçä¾èµæ¨¡åãSoï¼æä»¬å¯ä»¥è½¬æ¢ä¸ä¸ï¼ææ¨¡åAè®¾ç½®ä¸ºå ¶ä¾èµæ¨¡åçå¼ç¨æ¨¡åï¼å½ä¾èµæ¨¡åå è½½å®ææ¶éç¥Aæ¥æ§è¡å·¥å彿°å®ææè½½exportsã
æç»æä»¬çModule类设计æï¼
function Module(url, deps) {}
Module.prototype = {
constructor: Module,
load: function() {
var mod = this;
var args = [];
if (mod.status >= STATUS.LOAD) return mod;
mod.status = STATUS.LOAD;
mod.resolve();
mod.setDependents();
mod.checkCircular();
// about to execute/load dependencies
each(mod.dependencies, function(dep) {
if (dep.status < STATUS.FETCH) {
dep.fetch();
} else if (dep.status === STATUS.SAVE) {
dep.load();
} else if (dep.status >= STATUS.EXECUTED) {
args.push(dep.exports);
}
});
mod.status = STATUS.EXECUTING;
// means load all dependencies
if (args.length === mod.dependencies.length) {
args.push(mod.exports);
mod.makeExports(args);
mod.status = STATUS.EXECUTED;
mod.notifyDependents();
}
},
resolve: function() {},
setDependents: function() {},
checkCircular: function() {},
notifyDependents: function() {},
fetch: function() {},
onload: function(error) {},
save: function(deps) {}
}
3. 最终实现
https://github.com/creeperyang/amd-loader/blob/master/amd.js 有比较完整的注释,结合上面所讲的,应该比较容易理解。