前言
现在工作中基本离不开requireJS这种模块管理工具了,之前一直在用,但是对其原理不甚熟悉,整两天我们来试着学习其源码,而后在探寻其背后的AMD思想吧
于是今天的目标是熟悉requireJS整体框架结构,顺便看看之前的简单demo
程序入口
源码阅读仍然有一定门槛,通看的做法不适合我等素质的选手,所以还是得由入口开始,requireJS的入口便是引入时候指定的data-main
<script src="require.js" type="text/javascript" data-main="main.js"></script>
在js引入后,会自动执行指向data-main的js函数,这个就是我们所谓的入口,跟着这条线,我们就进入了requirejs的大门
首先,引入js文件本身不会干什么事情,那么requirejs内部做了什么呢?
① 除了一些初始化操作以为第一件干的事情,值执行这段代码:
//Create default context. req({});
这段代码会构造默认的参数,其调用的又是整个程序的入口
req = requirejs = function (deps, callback, errback, optional) {}
这里具体干了什么我们先不予关注,继续往后面走,因为貌似,这里与data-main暂时不相干,因为这段会先于data-main逻辑运行
然后,进入data-main相关的逻辑了:
//Look for a data-main script attribute, which could also adjust the baseUrl. if (isBrowser && !cfg.skipDataMain) { //Figure out baseUrl. Get it from the script tag with require.js in it. eachReverse(scripts(), function (script) { //Set the 'head' where we can append children by //using the script's parent. if (!head) { head = script.parentNode; } //Look for a data-main attribute to set main script for the page //to load. If it is there, the path to data main becomes the //baseUrl, if it is not already set. dataMain = script.getAttribute('data-main'); if (dataMain) { //Preserve dataMain in case it is a path (i.e. contains '?') mainScript = dataMain; //Set final baseUrl if there is not already an explicit one. if (!cfg.baseUrl) { //Pull off the directory of data-main for use as the //baseUrl. src = mainScript.split('/'); mainScript = src.pop(); subPath = src.length ? src.join('/') + '/' : './'; cfg.baseUrl = subPath; } //Strip off any trailing .js since mainScript is now //like a module name. mainScript = mainScript.replace(jsSuffixRegExp, ''); //If mainScript is still a path, fall back to dataMain if (req.jsExtRegExp.test(mainScript)) { mainScript = dataMain; } //Put the data-main script in the files to load. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; return true; } }); }
因为requireJS不止用于浏览器,所以这里有一个判断,我们暂时不予关注,看看他干了些什么
① 他会去除页面所有的script标签,然后倒叙遍历之
scripts() => [<script src="require.js" type="text/javascript" data-main="main.js"></script>]
这个地方遇到两个方法
eachReverse
与each一致,只不过由逆序遍历
scripts
便是document.getElementsByTagName('script');返回所有的script标签
然后开始的head便是html中的head标签,暂时不予理睬
dataMain = script.getAttribute('data-main');
然后这一句便可以获取当前指定运行的文件名,比如这里
dataMain => main.js
如果不存在就不会有什么操作了
PS:我原来记得默认指向main.js,看来是我记错了......
然后下来做了一些处理,会根据指定的main.js初步确定bashUrl,其实就是与main.js统一目录
最后做了关键的一个步骤:
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
将main放入带加载的配置中,而本身不干任何事情,继续接下来的逻辑......然后此逻辑暂时结束,根据这些参数进入下一步骤
req/requirejs
根据上一步骤的处理,会形成上面截图的参数,而后再一次执行入口函数req,这个时候就会发生不一样的事情了
/** * Main entry point. * * If the only argument to require is a string, then the module that * is represented by that string is fetched for the appropriate context. * * If the first argument is an array, then it will be treated as an array * of dependency string names to fetch. An optional function callback can * be specified to execute when all of those dependencies are available. * * Make a local req variable to help Caja compliance (it assumes things * on a require that are not standardized), and to give a short * name for minification/local scope use. */ req = requirejs = function (deps, callback, errback, optional) { //Find the right context, use default var context, config, contextName = defContextName; // Determine if have config object in the call. if (!isArray(deps) && typeof deps !== 'string') { // deps is a config object config = deps; if (isArray(callback)) { // Adjust args if there are dependencies deps = callback; callback = errback; errback = optional; } else { deps = []; } } if (config && config.context) { contextName = config.context; } context = getOwn(contexts, contextName); if (!context) { context = contexts[contextName] = req.s.newContext(contextName); } if (config) { context.configure(config); } return context.require(deps, callback, errback); };
这个时候我们的第一个参数deps就不再是undefined了,而是一个对象,这里便将其配置放到了config变量中保持deps为一数组,然后干了些其他事情
这里有个变量context,需要特别注意,后面我们来看看他有些什么,这里有一个新的函数
function getOwn(obj, prop) { return hasProp(obj, prop) && obj[prop]; } function hasProp(obj, prop) { return hasOwn.call(obj, prop); } hasOwn = op.hasOwnProperty
这里会获取非原型属性将其扩展,首次执行时候会碰到一个非常重要的函数newContext 因为他是一个核心,我们这里暂时选择忽略,不然整个全部就陷进去了
经过newContext处理后的context就变成这个样子了:
if (config) { context.configure(config); }
这里就会将我们第一步的参数赋值进对象,具体干了什么,我们依旧不予理睬,main.js干了两件事情:
① 暂时性设置了baseUrl
② 告诉requireJS你马上要加载我了
于是最后终于调用require开始处理逻辑
return context.require(deps, callback, errback);
require
因为context.require = context.makeRequire();而该函数本身又返回localRequire函数,所以事实上这里是执行的localRequire函数,内部维护着一个闭包
因为nextContext只会运行一次,所以很多require实际用到的变量都是nextContext闭包所维护,比如我们这里便可以使用config变量
这里依旧有一些特殊处理,比如deps是字符串的情况,但是我们暂时不予关注.......
PS:搞了这么久很多不予关注了,欠了很多帐啊!
他这里应该是有一个BUG,所以这里用到了一个settimeout延时
PS:因为settimeout的使用,整个这块的程序全部会抛到主干逻辑之后了
然后接下来的步骤比较关键了,我们先抛开一切来理一理这个newContext
newContext
newContext占了源码的主要篇幅,他也只会在初始化时候执行一次,而后便不再执行了:
if (!context) { context = contexts[contextName] = req.s.newContext(contextName); }
现在,我们就目前而知来简单理一理,requireJS的结构
① 变量声明,工具类
在newContext之前,完全是做一些变量的定义,或者做一些简单的操作,里面比较关键的是contexts/cfg对象,会被后面无数次的用到
② 实例化上下文/newContext
紧接着就是newContext这洋洋洒洒一千多行代码了,其中主要干了什么暂时不知道,据我观察应该是做环境相关的准备
③ 对外接口
上面操作结束后便提供了几个主要对外接口
requirejs
require.config
虽然这里是两个函数,其实都是requirejs这一关入口
而后,require自己撸了一把,实例化了默认的参数,这里便调用了newContext,所以以后都不会调用,其中的函数多处于其闭包环境
接下来根据引入script标签的data-main做了一次文章,初始化了简单的参数,并将main.js作为了依赖项,这里会根据main.js重写cfg对象
最后requirejs执行一次reg(cfg),便真的开始了所有操作,这个时候我们就进入newContext,看看他主要干了什么
PS:所有require并未提供任何借口出来,所以在全局想查看其contexts或者cfg是不行的,而且每次操作都可能导致其改变
要了解newContext函数,还是需要进入其入口
if (!context) { context = contexts[contextName] = req.s.newContext(contextName); }
从script标签引入require库时候,会因为这段代码执行一次newContext函数,从此后,该函数不会被执行,其实现的原因不是我等现在能明白的,先看懂实现再说吧
//Create default context. req({});
所以上面说了那么多,看了这么久,其实最关键的还是首次加载,首次加载就决定了运行上下文了
整体结构
newContext的基本结构大概是这样:
① 函数作用域内变量定义(中间初始化了一发handlers变量)
② 一堆工具函数定义
③ Module模块(这块给人的感觉不明觉厉...应该是核心吧)
④ 实例化context对象,将该对象返回,然后基本结束
进入newContext后,第一步是基本变量定义,这种对外的框架一般都不会到处命名变量,而是将所有变量全部提到函数最前面
一来是js解析时候声明本身会提前,而来可能是到处命名变量会让我们找不到吧......
开始定义了很多变量,我们一来都不知道是干神马的,但是config变量却引起了我们的注意,这里先放出来,继续往下就是一连串的函数了,值得说明的是,这些变量会被重复利用哦
一眼看下来,该函数本身并没有做什么实际的事情,这个时候我们就需要找其入口,这里的入口是
//首次调用 req({}) => //触发newContext,做首次初始化并返回给context对象 context = contexts[contextName] = req.s.newContext(contextName) => //注意这里require函数其实处于了mackRequire函数的闭包环境 context.require = context.makeRequire(); => //首次调用newContext返回对象初始化变量 context.configure(config);
所以,在首次初始化后,并未做特别的处理,直到configure的调用,于是让我们进入该函数
首次传入的是空对象,所以开始一段代码暂时没有意义,这里使用的config变量正是newContext维护的闭包,也就是上面让注意的
config = { //Defaults. Do not set a default for map //config to speed up normalize(), which //will run faster if there is no default. waitSeconds: 7, baseUrl: './', paths: {}, pkgs: {}, shim: {}, config: {} },
下面用到了一个新的函数:
eachProp
这个函数会遍历对象所有非原型属性,并且使用第二个参数(函数)执行之,如果返回true便停止,首次执行时候cfg为空对象,便没有往下走,否则config变量会被操作,具体我们暂时不管
这个所谓的入口执行后实际的意义基本等于什么都没有干......
但是,这里可以得出一个弱弱的结论就是
configure是用于设置参数滴
所以所谓的入口其实没有干事情,这个时候第二个入口便出现了
context.require
return context.require(deps, callback, errback);
参数设置结束后便会执行context的require方法,这个是真正的入口,他实际调用顺序为:
context.require = context.makeRequire(); => localRequire
所以真正调用localRequire时候,已经执行了一番makeRequire函数了,现在处于了其上下文,正因为localRequire被处理过,其多了几个函数属性
除此之外,暂时没有看出其它变化,所以这里在某些特定场景是等价的
过程中会执行一次intakeDefines,他的意义是定义全局队列,其意义暂时不明,然后进入了前面说的那个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代码,比如整个
return context.require(deps, callback, errback);
执行了后上面才会慢慢执行
PS:require这段比较重要,留待明天分析,今天先看整体逻辑
下面的主要逻辑又到了这里
requireMod = getModule(makeModuleMap(null, relMap));
我们这里主要先看getModule先,首先makeModuleMap比较关键,他会根据规则创建一些模块唯一标识的东西,暂时是什么当然是先不管啦......
PS:其规则应该与加载的require数量有关,最后会形成这个东西
然后是我们关键的getModule函数
function getModule(depMap) { var id = depMap.id, mod = getOwn(registry, id); if (!mod) { mod = registry[id] = new context.Module(depMap); } return mod; }
可以看到,一旦我们加载了一个模块便不会重新加载了,这是一个很重要的发现哦
registry
该全局变量用于存储加载模块的键值对
第一步当然是加载啦,但是首次应该会跳过,因为当然事实上没有需要加载的模块,一起跟下去吧
Module
然后进入我们关键的Module类模块了
总的来说,这个模块还是很长的,首先是其构造函数
这里仍有很多东西读不懂,所以就全部过吧,反正今天的主要目的是熟悉整体框架
这里实例化结束后便形成了一个模块暂存于requireMod变量中,函数执行结束后变量会销毁,该模块会存与全局registery对象中
这里会执行其init方法干具体业务的事情
requireMod.init(deps, callback, errback, { enabled: true });
这里又会执行
this.enable();
然后又会调用 this.check();这个家伙操作结束后接下来checkLoaded就会创建script标签了......
然后今天累了,明天继续吧......
结语
今天的目标是熟悉requireJS的整体结构,如果没有错觉或者解读失误,我们应该大概了解了requireJS的整体结构,于是让我们明天继续吧
PS:尼玛这个框架还真是有点难,小钗感觉有点小吃力啊,估计要读到下周才能真正理解一点的了.......
本文转自叶小钗博客园博客,原文链接:http://www.cnblogs.com/yexiaochai/p/3632580.html,如需转载请自行联系原作者