【requireJS源码学习01】了解整个requireJS的结构

简介:

前言

现在工作中基本离不开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一致,只不过由逆序遍历

  View Code

scripts

便是document.getElementsByTagName('script');返回所有的script标签

然后开始的head便是html中的head标签,暂时不予理睬

  View Code
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的调用,于是让我们进入该函数

  View Code

首次传入的是空对象,所以开始一段代码暂时没有意义,这里使用的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变量会被操作,具体我们暂时不管

  View Code

这个所谓的入口执行后实际的意义基本等于什么都没有干......

但是,这里可以得出一个弱弱的结论就是

configure是用于设置参数滴

所以所谓的入口其实没有干事情,这个时候第二个入口便出现了

context.require

return context.require(deps, callback, errback);

参数设置结束后便会执行context的require方法,这个是真正的入口,他实际调用顺序为:

context.require = context.makeRequire();
=>
localRequire

所以真正调用localRequire时候,已经执行了一番makeRequire函数了,现在处于了其上下文,正因为localRequire被处理过,其多了几个函数属性

除此之外,暂时没有看出其它变化,所以这里在某些特定场景是等价的

  View Code

过程中会执行一次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数量有关,最后会形成这个东西

  View Code

然后是我们关键的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类模块了

  View Code

总的来说,这个模块还是很长的,首先是其构造函数

这里仍有很多东西读不懂,所以就全部过吧,反正今天的主要目的是熟悉整体框架

这里实例化结束后便形成了一个模块暂存于requireMod变量中,函数执行结束后变量会销毁,该模块会存与全局registery对象中

这里会执行其init方法干具体业务的事情

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

这里又会执行

this.enable();
  View Code

然后又会调用 this.check();这个家伙操作结束后接下来checkLoaded就会创建script标签了......

  View Code

然后今天累了,明天继续吧......

结语

今天的目标是熟悉requireJS的整体结构,如果没有错觉或者解读失误,我们应该大概了解了requireJS的整体结构,于是让我们明天继续吧

PS:尼玛这个框架还真是有点难,小钗感觉有点小吃力啊,估计要读到下周才能真正理解一点的了.......




本文转自叶小钗博客园博客,原文链接:http://www.cnblogs.com/yexiaochai/p/3632580.html,如需转载请自行联系原作者

相关文章
|
6月前
|
JavaScript 前端开发 编译器
js开发: 请解释什么是Babel,以及它在项目中的作用。
**Babel是JavaScript编译器,将ES6+代码转为旧版JS以保证兼容性。它用于前端项目,功能包括语法转换、插件扩展、灵活配置和丰富的生态系统。Babel确保新特性的使用而不牺牲浏览器支持。** ```markdown - Babel: JavaScript编译器,转化ES6+到兼容旧环境的JS - 保障新语法在不同浏览器的运行 - 支持插件,扩展编译功能 - 灵活配置,适应项目需求 - 富强的生态系统,多样化开发需求 ```
56 4
|
6月前
|
JavaScript 前端开发 Shell
一文搞懂nodejs和JS的模块化
一文搞懂nodejs和JS的模块化
59 0
|
JavaScript 前端开发 Go
requireJS的基本用法(上)
requireJS的基本用法
167 0
requireJS的基本用法(上)
|
JavaScript 前端开发 C#
requireJS的基本用法(下)
requireJS的基本用法(下)
155 0
requireJS的基本用法(下)
|
JavaScript 前端开发 API
一文搞懂JS模块、模块格式、模块加载器和模块打包器(中)
接下来我们就来一起学习下js模块、模块化解决方案、模块加载器和模块打包器的区别。 本文的主要意图是帮大家快速理解现代前端JS开发的概念,并不会深入去探讨每种工具和模式。
|
JavaScript 前端开发 API
一文搞懂JS模块、模块格式、模块加载器和模块打包器(上)
接下来我们就来一起学习下js模块、模块化解决方案、模块加载器和模块打包器的区别。 本文的主要意图是帮大家快速理解现代前端JS开发的概念,并不会深入去探讨每种工具和模式。
|
JavaScript 前端开发 API
一文搞懂JS模块、模块格式、模块加载器和模块打包器(下)
接下来我们就来一起学习下js模块、模块化解决方案、模块加载器和模块打包器的区别。 本文的主要意图是帮大家快速理解现代前端JS开发的概念,并不会深入去探讨每种工具和模式。
|
JavaScript 前端开发 应用服务中间件
|
Web App开发 JavaScript 前端开发