对于现在的前端生态来说,requirejs是有点过时了,webpack帮我们包干了一切。但是对于学习源码这件事情来说,永远是不过时的!

最近稍微闲下来了一点,就着以前做过的项目,我也来看看requirejs的源码。希望能涨点姿势!

1.html中的data-main是个什么鬼?

  1. //address.html
    <script type="text/javascript" data-main="${base}/static/js/app/userCenter/address" src="${base}/static/js/plugins/require.js"></script>

使用requirejs,在我们的页面需要引入一个有data-main的主入口js文件。

既然这样,我们就去require源码中去找找data-main在哪里出现了。

  1. //Look for a data-main script attribute, which could also adjust the baseUrl.去寻找一个data-main的script属性,并且能够匹配baseUrl
  2. if (isBrowser && !cfg.skipDataMain) {
  3. //Figure out baseUrl. Get it from the script tag with require.js in it.计算出baseUrl.从含有require.js的script标签中获取它.
  4. eachReverse(scripts(), function (script) {
  5. //Set the 'head' where we can append children by
  6. //using the script's parent.
  7. if (!head) {
  8. head = script.parentNode;
  9. }
  10.  
  11. //Look for a data-main attribute to set main script for the page
  12. //to load. If it is there, the path to data main becomes the
  13. //baseUrl, if it is not already set.
  14. dataMain = script.getAttribute('data-main');
  15. if (dataMain) {
  16. //Preserve dataMain in case it is a path (i.e. contains '?')
  17. mainScript = dataMain;
  18.  
  19. //Set final baseUrl if there is not already an explicit one,
  20. //but only do so if the data-main value is not a loader plugin
  21. //module ID.
  22. if (!cfg.baseUrl && mainScript.indexOf('!') === -1) {
  23. //Pull off the directory of data-main for use as the
  24. //baseUrl.
  25. src = mainScript.split('/');
  26. mainScript = src.pop();
  27. subPath = src.length ? src.join('/') + '/' : './';
  28.  
  29. cfg.baseUrl = subPath;
  30. }
  31.  
  32. //Strip off any trailing .js since mainScript is now
  33. //like a module name.
  34. mainScript = mainScript.replace(jsSuffixRegExp, '');
  35.  
  36. //If mainScript is still a path, fall back to dataMain
  37. if (req.jsExtRegExp.test(mainScript)) {
  38. mainScript = dataMain;
  39. }
  40.  
  41. //Put the data-main script in the files to load.
  42. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
  43.  
  44. return true;
  45. }
  46. });
  47. }

我们在源码中找到了6处匹配的地方,全部在上面这段代码中.

这里用到了一个公有方法eachReverse,包含两个参数,ary和func,func是回调函数,回调函数接受三个参数(数组的每一项,数组的索引,完整的数组元素).

  1. /**
  2. * Helper function for iterating over an array backwards. If the func 帮助函数为了倒序遍历数组,如果func返回true,则跳出循环
  3. * returns a true value, it will break out of the loop.
  4. */
  5. function eachReverse(ary, func) {
  6. if (ary) {
  7. var i;
  8. for (i = ary.length - 1; i > -1; i -= 1) {
  9. if (ary[i] && func(ary[i], i, ary)) {
  10. break;
  11. }
  12. }
  13. }
  14. }

eachReverse的ary是一个scripts()方法返回的数组。所以接下来去看看scripts方法。scripts方法取到html上所有的script标签.

  1. function scripts() {
  2. return document.getElementsByTagName('script');
  3. }

通过script取到data-main属性的值。我们可以看到dataMain变量的值就是address.html中data-main属性的值。

接下来的操作都是对url地址的一些处理.通过src.pop()取得mainScript的值为address.

再将地址拼接起来取得子目录。可以看到subPath少了前面的/address目录,subPath被赋值给了cfg.baseUrl属性。

  1. jsSuffixRegExp = /\.js$/,
  2.  
  3. //Strip off any trailing .js since mainScript is now
  4. //like a module name.
  5. //剥去任何.js结尾的mainScript,使得它看起来像一个模块的名称
  6. mainScript = mainScript.replace(jsSuffixRegExp, '');

通过正则匹配任何已.js结尾的文件。例如上面的address.html的data-main如果变成:xxxxx/address.js ,这里就会把.js给替换掉,如同注释中字面意义的“模块化”。

到这里的话,对data-main的处理算完结了。正如data-main是我们的主模块,address.html的主模块就是deps里的address。

但是要说一点的就是这里的cfg对象是要在req({});初始化执行上下文以后才会需要用到。这里只是按照我们正常思维打断点先想到的。

2.js里面怎么跑

注释上写到这里是程序的主入口,相当于构造函数,那我们就来看一下。

  1. /**
  2. * Main entry point.主入口
  3. *
  4. * If the only argument to require is a string, then the module that
  5. * is represented by that string is fetched for the appropriate context.
  6. *
  7. * If the first argument is an array, then it will be treated as an array
  8. * of dependency string names to fetch. An optional function callback can
  9. * be specified to execute when all of those dependencies are available.
  10. *
  11. * Make a local req variable to help Caja compliance (it assumes things 创建一个局部req变量去帮助caja compliance,这个caja貌似说的是一个google的caja库,类似创建了一个虚拟的iframe,并且给一个短名称的局部作用域去使用。
  12. * on a require that are not standardized), and to give a short
  13. * name for minification/local scope use.
  14. */
  15. req = requirejs = function (deps, callback, errback, optional) {
  16.  
  17. //Find the right context, use default
  18. var context, config,
  19. contextName = defContextName;
  20.  
  21. // Determine if have config object in the call.
  22. if (!isArray(deps) && typeof deps !== 'string') {
  23. // deps is a config object deps是一个配置对象
  24. config = deps;
  25. if (isArray(callback)) {
  26. // Adjust args if there are dependencies
  27. deps = callback;
  28. callback = errback;
  29. errback = optional;
  30. } else {
  31. deps = [];
  32. }
  33. }
  34.  
  35. if (config && config.context) {
  36. contextName = config.context;
  37. }
  38.  
  39. context = getOwn(contexts, contextName);
  40. if (!context) {
  41. context = contexts[contextName] = req.s.newContext(contextName);
  42. }
  43.  
  44. if (config) {
  45. context.configure(config);
  46. }
  47.  
  48. return context.require(deps, callback, errback);
  49. };

在随后的代码中,执行了req并且传入一个空对象,这里就创建了req这个函数执行的上下文。

  1. //Create default context.
  2. req({});

这里用到了getOwn函数,getOwn要配合hasProp使用。先检查是否包含实例属性,如果包含的话就将属性赋值到目标对象。

  1. function hasProp(obj, prop) {
  2. return hasOwn.call(obj, prop);
  3. }
  4.  
  5. function getOwn(obj, prop) {
  6. return hasProp(obj, prop) && obj[prop];
  7. }

因为context为false,所以newContext进行了初始化。

  1. s = req.s = {
  2. contexts: contexts,
  3. newContext: newContext
  4. };

newContext的代码非常的多,差不多1500行左右。

newContext大致结构如下:

1.一些工具方法:例如trimDots。

2.处理模块的方法:例如normalize等

3.创建并保存了require的运行环境:context对象中的方法

4.创建了require的模块:Module构造函数

这里context对象调用了makeRequire方法。

  1. context.require = context.makeRequire();
  2. return context;
  1. //简化后的代码,可以很明显的看出,为了形成闭包
  2. makeRequire:function(){
  3. function localRequire(){
  4. //TODO
  5. return localRequire;
  6. }
  7. return localRequire;
  8. }

通过一个mixin方法实现了属性拷贝。

  1. /**
  2. * Simple function to mix in properties from source into target, 简单的方法把源对象的属性混合进目标对象中,仅在目标对象并没有相同属性名称的情况下
  3. * but only if target does not already have a property of the same name.
  4. */
  5. function mixin(target, source, force, deepStringMixin) {
  6. if (source) {
  7. eachProp(source, function (value, prop) {
  8. if (force || !hasProp(target, prop)) {
  9. if (deepStringMixin && typeof value === 'object' && value &&
  10. !isArray(value) && !isFunction(value) &&
  11. !(value instanceof RegExp)) {
  12.  
  13. if (!target[prop]) {
  14. target[prop] = {};
  15. }
  16. mixin(target[prop], value, force, deepStringMixin);
  17. } else {
  18. target[prop] = value;
  19. }
  20. }
  21. });
  22. }
  23. return target;
  24. }

最后返回的target,也就是我们localRequire,添加了4个属性,这里我们可以看出来,它是返回了函数localRequire的闭包。

又给localRequire这个闭包再添加了一个属性,undef

并将闭包赋值给context.require。随后返回context这个对象。

然后我们会进入configure这个方法,因为第一次初始化是传入的一个空对象,所以这里对配置的处理并没有什么实际意义,我们暂且略过。在第二次有具体参数传入了再具体说明。

最后将在context对象中维护的localRequire闭包执行并返回。

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

我们会碰到nextTick这样一个方法,req.nextTick将匿名函数添加到事件队列中去,异步的去执行它,而这里的匿名函数的功能就是去异步的加载require的模块。但是为何这里与前一次异步延时设置为4,我觉得1,2,3应该都是可以的,这里不是很清楚!如果有朋友了解,可以解释一下
不过这里的注释还是很好笑的:如果有比setTimeout更好的方法,那么就去重写它。然后用的名称叫nextTick,就是在Node中为了解决setTimeout存在问题的方法。大家有兴趣的话可以去看看《异步编程》。

  1. /**
  2. * Execute something after the current tick
  3. * of the event loop. Override for other envs
  4. * that have a better solution than setTimeout.
  5. * @param {Function} fn function to execute later.
  6. */
  7. req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
  8. setTimeout(fn, 4);
  9. } : function (fn) { fn(); };

继续往下走,我们看到了通过mixin方法添加到闭包的4个属性,这里把这4个属性给暴露给了外层的req对象。

  1. //Exports some context-sensitive methods on global require.
  2. each([
  3. 'toUrl',
  4. 'undef',
  5. 'defined',
  6. 'specified'
  7. ], function (prop) {
  8. //Reference from contexts instead of early binding to default context,
  9. //so that during builds, the latest instance of the default context
  10. //with its config gets used.
  11. req[prop] = function () {
  12. var ctx = contexts[defContextName];
  13. return ctx.require[prop].apply(ctx, arguments);
  14. };
  15. });

随后会执行我前面提到的处理data-main这块的代码。当所有的准备工作做好了以后,

在这里就将我们前面通过data-main拿到的cfg对象传进去。

  1. //Set up with config info.
  2. req(cfg);

3.小结一下

req({}) => req(cfg);

这一段流程走过以后,我们发现最大的改变就是contexts这个对象。

=>

而这些改变最重要的目的就是创建一个适合require运行的上下文环境。当然通过makeRequire创建的闭包函数ocalRequire,它也是不同的,因为后面的逻辑不同,传入的参数不同,形成了不同的闭包。

这几天require读下来,感觉没那么好懂,果然还是水平不够,先好好消化一下。下次再来继续啃.

【源码学习】之requirejs的更多相关文章

  1. 【iScroll源码学习04】分离IScroll核心

    前言 最近几天我们前前后后基本将iScroll源码学的七七八八了,文章中未涉及的各位就要自己去看了 1. [iScroll源码学习03]iScroll事件机制与滚动条的实现 2. [iScroll源码 ...

  2. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  3. jQuery源码学习感想

    还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...

  4. MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)

    前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...

  5. MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)

    前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...

  6. MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)

    前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...

  7. MVC系列——MVC源码学习:打造自己的MVC框架(一:核心原理)

    前言:最近一段时间在学习MVC源码,说实话,研读源码真是一个痛苦的过程,好多晦涩的语法搞得人晕晕乎乎.这两天算是理解了一小部分,这里先记录下来,也给需要的园友一个参考,奈何博主技术有限,如有理解不妥之 ...

  8. 我的angularjs源码学习之旅2——依赖注入

    依赖注入起源于实现控制反转的典型框架Spring框架,用来削减计算机程序的耦合问题.简单来说,在定义方法的时候,方法所依赖的对象就被隐性的注入到该方法中,在方法中可以直接使用,而不需要在执行该函数的时 ...

  9. ddms(基于 Express 的表单管理系统)源码学习

    ddms是基于express的一个表单管理系统,今天抽时间看了下它的代码,其实算不上源码学习,只是对它其中一些小的开发技巧做一些记录,希望以后在项目开发中能够实践下. 数据层封装 模块只对外暴露mod ...

  10. leveldb源码学习系列

    楼主从2014年7月份开始学习<>,由于书籍比较抽象,为了加深思考,同时开始了Google leveldb的源码学习,主要是想学习leveldb的设计思想和Google的C++编程规范.目 ...

随机推荐

  1. zabbix_agent-linux下的安装

    scp 10.25.133.184:/usr/local/zabbix-2.4.1.tar.gz /usr/local 1.为了安全考虑zabbix只使用普通用户运行,假如你当前用户叫ttlsa,那么 ...

  2. a里面不能嵌套a

    1. <a href=""> <a href=""></a></a> 会被浏览器解析为 2. <a hre ...

  3. cat: can't open '/lib/modules/2.6.35.3-571-gcca29a0/modules.dep': No such file or directory

    在使用modprobe 或者modinfo  cat: can't open '/lib/modules/2.6.35.3-571-gcca29a0/modules.dep': No such fil ...

  4. XJOI1689相连的城市

    相连的城市 n个城市中,某些城市间有道路互相连接.给出与每个城市相邻的城市有多少个,请输出城市间的邻接矩阵. 输入格式: 第一行输入一个正整数n,表示城市的个数. 第二行输入n个用空格隔开的非负整数, ...

  5. 【Egret】WebSocket 的使用说明

    在Egret里可以使用WebSocket ,也可以使用socket.io 首先先深入了解一下 WebSocket 在Egret里的机制,看这篇文章: 主要讲解Egret里使用WebSocket和pro ...

  6. 老李分享:android手机测试之适配(1)

    Android的屏幕适配一直以来都在折磨着我们这些开发者,本篇文章以Google的官方文档为基础,全面而深入的讲解了Android屏幕适配的原因.重要概念.解决方案及最佳实践,我相信如果你能认真的学习 ...

  7. 老李分享:qtp自动化测试框架赏析-关键字自动化测试框架

    老李分享:qtp自动化测试框架赏析-关键字自动化测试框架   QTP从2005年继winrunner,robot逐渐退出历史舞台之后,占领主流自动化测试工具市场已经10年之久.当初为了提高在自动化测试 ...

  8. number问题

    Missing Number Given an array containing n distinct numbers taken from 0, 1, 2, ..., n, find the one ...

  9. 4.Maven仓库

    1. 何为Maven仓库 Maven仓库就是统一存放所有依赖的地方,其他所有项目都可以在仓库里通过坐标找到所需要的依赖. 2. 仓库的布局 任何一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中 ...

  10. python+robot framework实现测报告定制化和邮件发送

    前面已经介绍了python+robot framework自动化框架和基本原理的实现,详情请看 python+robot framework接口自动化测试 本章主要讲解报告已经产生那如何以自动化的方式 ...