`

理解requireJS-实现一个简单的模块加载器

 
阅读更多

http://www.cnblogs.com/yexiaochai/p/3961291.html

 在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题:

① 解决单文件变量命名冲突问题

② 解决前端多人协作问题

③ 解决文件依赖问题

④ 按需加载(这个说法其实很假了)

⑤ ......

为了深入了解加载器,中间阅读过一点requireJS的源码,但对于很多同学来说,对加载器的实现依旧不太清楚

事实上不通过代码实现,单单凭阅读想理解一个库或者框架只能达到一知半解的地步,所以今天便来实现一个简单的加载器

加载器原理分析

分与合

事实上,一个程序运行需要完整的模块,以下代码为例:

复制代码
 1   //求得绩效系数
 2   var performanceCoefficient = function () {
 3     return 0.2;
 4   };
 5 
 6   //住房公积金计算方式
 7   var companyReserve = function (salary) {
 8     return salary * 0.2;
 9   };
10 
11   //个人所得税
12   var incomeTax = function (salary) {
13     return salary * 0.2;
14   };
15 
16   //基本工资
17   var salary = 1000;
18 
19   //最终工资
20   var mySalary = salary + salary * performanceCoefficient();
21   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
22   console.log(mySalary);
复制代码

我一份完整的工资来说,公司会有绩效奖励,但是其算法可能非常复杂,其中可能涉及到出勤率,完成度什么的,这里暂时不管

而有增便有减,所以我们会交住房公积金,也会扣除个人所得税,最终才是我的工资

对于完整的程序来说上面的流程缺一不可,但是各个函数中却有可能异常的复杂,跟钱有关系的东西都复杂,所以单单是公司绩效便有可能超过1000行代码

于是我们这边便会开始分:

复制代码
 1 <script src="companyReserve.js" type="text/javascript"></script>
 2 <script src="incomeTax.js" type="text/javascript"></script>
 3 <script src="performanceCoefficient.js" type="text/javascript"></script>
 4 <script type="text/javascript">
 5 
 6   //基本工资
 7   var salary = 1000;
 8 
 9   //最终工资
10   var mySalary = salary + salary * performanceCoefficient();
11   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
12   console.log(mySalary);
13 
14 </script>
复制代码

上面的代码表明上是“分”开了,事实上也造成了“合”的问题,我要如何才能很好的把它们重新合到一起呢,毕竟其中的文件可能还涉及到依赖,这里便进入我们的require与define

require与define

事实上,上面的方案仍然是以文件划分,而不是以模块划分的,若是文件名发生变化,页面会涉及到改变,其实这里应该有一个路径的映射处理这个问题

var pathCfg = {
  'companyReserve': 'companyReserve',
  'incomeTax': 'incomeTax',
  'performanceCoefficient': 'performanceCoefficient'
};

于是我们一个模块便对应了一个路径js文件,剩下的便是将之对应模块的加载了,因为前端模块涉及到请求。所以这种写法:

companyReserve = requile('companyReserve');

对于前端来说是不适用的,就算你在哪里看到这样做了,也一定是其中做了一些“手脚”,这里我们便需要依据AMD规范了:

复制代码
 1 require.config({
 2   'companyReserve': 'companyReserve',
 3   'incomeTax': 'incomeTax',
 4   'performanceCoefficient': 'performanceCoefficient'
 5 });
 6 
 7 require(['companyReserve', 'incomeTax', 'performanceCoefficient'], function (companyReserve, incomeTax, performanceCoefficient) {
 8   //基本工资
 9   var salary = 1000;
10 
11   //最终工资
12   var mySalary = salary + salary * performanceCoefficient();
13   mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
14   console.log(mySalary);
15 });
复制代码

这里便是一个标准的requireJS的写法了,首先定义模块以及其路径映射,其中定义依赖项

require(depArr, callback)

一个简单完整的模块加载器基本就是这个样子了,首先是一个依赖的数组,其次是一个回调,回调要求依赖项全部加载才能运行,并且回调的参数便是依赖项执行的结果,所以一般要求define模块具有一个返回值

方案有了,那么如何实现呢?

实现方案

说到模块加载,人们第一反应都是ajax,因为无论何时,能拿到模块文件的内容,都是模块化的基本,但是采用ajax的方式是不行的,因为ajax有跨域的问题

而模块化方案又不可避免的要处理跨域的问题,所以使用动态创建script标签加载js文件便成为了首选,但是,不使用ajax的方案,对于实现难度来说还是有要求

PS:我们实际工作中还会有加载html模板文件的场景,这个稍候再说

通常我们是这样做的,require作为程序入口,调度javascript资源,而加载到各个define模块后,各个模块便悄无声息的创建script标签加载

加载结束后便往require模块队列报告自己加载结束了,当require中多有依赖模块皆加载结束时,便执行其回调

原理大致如此,剩下的只是具体实现,而后在论证这个理论是否靠谱即可

加载器阉割实现

核心模块

根据以上理论,我们由整体来说,首先以入口三个基本函数来说

var require = function () {
};
require.config = function () {
};
require.define = function () {
};

这三个模块比不可少:

① config用以配置模块与路径的映射,或者还有其他用处

② require为程序入口

③ define设计各个模块,响应require的调度

然后我们这里会有一个创建script标签的方法,并且会监听其onLoad事件

④ loadScript

其次我们加载script标签后,应该有一个全局的模块对象,用于存储已经加载好的模块,于是这里提出了两个需求:

⑤ require.moduleObj 模块存储对象

⑥ Module,模块的构造函数

有了以上核心模块,我们形成了如下代码:

复制代码
 1 (function () {
 2 
 3   var Module = function () {
 4     this.status = 'loading'; //只具有loading与loaded两个状态
 5     this.depCount = 0; //模块依赖项
 6     this.value = null; //define函数回调执行的返回
 7   };
 8 
 9 
10   var loadScript = function (url, callback) {
11 
12   };
13 
14   var config = function () {
15 
16   };
17 
18   var require = function (deps, callback) {
19 
20   };
21 
22   require.config = function (cfg) {
23 
24   };
25 
26   var define = function (deps, callback) {
27 
28   };
29 
30 })();
复制代码

于是接下来便是具体实现,然后在实现过程中补足不具备的接口与细节,往往在最后的实现与最初的设计没有半毛钱关系......

代码实现

这块最初实现时,本来想直接参考requireJS的实现,但是我们老大笑眯眯的拿出了一个他写的加载器,我一看不得不承认有点妖

于是这里便借鉴了其实现,做了简单改造:

复制代码
  1 (function () {
  2 
  3   //存储已经加载好的模块
  4   var moduleCache = {};
  5 
  6   var require = function (deps, callback) {
  7     var params = [];
  8     var depCount = 0;
  9     var i, len, isEmpty = false, modName;
 10 
 11     //获取当前正在执行的js代码段,这个在onLoad事件之前执行
 12     modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN';
 13 
 14     //简单实现,这里未做参数检查,只考虑数组的情况
 15     if (deps.length) {
 16       for (i = 0, len = deps.length; i < len; i++) {
 17         (function (i) {
 18           //依赖加一
 19           depCount++;
 20           //这块回调很关键
 21           loadMod(deps[i], function (param) {
 22             params[i] = param;
 23             depCount--;
 24             if (depCount == 0) {
 25               saveModule(modName, params, callback);
 26             }
 27           });
 28         })(i);
 29       }
 30     } else {
 31       isEmpty = true;
 32     }
 33 
 34     if (isEmpty) {
 35       setTimeout(function () {
 36         saveModule(modName, null, callback);
 37       }, 0);
 38     }
 39 
 40   };
 41 
 42   //考虑最简单逻辑即可
 43   var _getPathUrl = function (modName) {
 44     var url = modName;
 45     //不严谨
 46     if (url.indexOf('.js') == -1) url = url + '.js';
 47     return url;
 48   };
 49 
 50   //模块加载
 51   var loadMod = function (modName, callback) {
 52     var url = _getPathUrl(modName), fs, mod;
 53 
 54     //如果该模块已经被加载
 55     if (moduleCache[modName]) {
 56       mod = moduleCache[modName];
 57       if (mod.status == 'loaded') {
 58         setTimeout(callback(this.params), 0);
 59       } else {
 60         //如果未到加载状态直接往onLoad插入值,在依赖项加载好后会解除依赖
 61         mod.onload.push(callback);
 62       }
 63     } else {
 64 
 65       /*
 66       这里重点说一下Module对象
 67       status代表模块状态
 68       onLoad事实上对应requireJS的事件回调,该模块被引用多少次变化执行多少次回调,通知被依赖项解除依赖
 69       */
 70       mod = moduleCache[modName] = {
 71         modName: modName,
 72         status: 'loading',
 73         export: null,
 74         onload: [callback]
 75       };
 76 
 77       _script = document.createElement('script');
 78       _script.id = modName;
 79       _script.type = 'text/javascript';
 80       _script.charset = 'utf-8';
 81       _script.async = true;
 82       _script.src = url;
 83 
 84       //这段代码在这个场景中意义不大,注释了
 85       //      _script.onload = function (e) {};
 86 
 87       fs = document.getElementsByTagName('script')[0];
 88       fs.parentNode.insertBefore(_script, fs);
 89 
 90     }
 91   };
 92 
 93   var saveModule = function (modName, params, callback) {
 94     var mod, fn;
 95 
 96     if (moduleCache.hasOwnProperty(modName)) {
 97       mod = moduleCache[modName];
 98       mod.status = 'loaded';
 99       //输出项
100       mod.export = callback ? callback(params) : null;
101 
102       //解除父类依赖,这里事实上使用事件监听较好
103       while (fn = mod.onload.shift()) {
104         fn(mod.export);
105       }
106     } else {
107       callback && callback.apply(window, params);
108     }
109   };
110 
111   window.require = require;
112   window.define = require;
113 
114 })();
复制代码

首先这段代码有一些问题:

没有处理参数问题,字符串之类皆未处理

未处理循环依赖问题

未处理CMD写法

未处理html模板加载相关

未处理参数配置,baseUrl什么都没有搞

基于此想实现打包文件也不可能

......

但就是这100行代码,便是加载器的核心,代码很短,对各位理解加载器很有帮助,里面有两点需要注意:

① requireJS是使用事件监听处理本身依赖,这里直接将之放到了onLoad数组中了

② 这里有一个很有意思的东西

document.currentScript

这个可以获取当前执行的代码段

requireJS是在onLoad中处理各个模块的,这里就用了一个不一样的实现,每个js文件加载后,都会执行require(define)方法

执行后便取到当前正在执行的文件,并且取到文件名加载之,正因为如此,连script的onLoad事件都省了......

demo实现

复制代码
 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3   <title></title>
 4 </head>
 5 <body>
 6 </body>
 7 <script src="require.js" type="text/javascript"></script>
 8 <script type="text/javascript">
 9   require(['util', 'math', 'num'], function (util, math, num) {
10 
11     num = math.getRadom() + '_' + num;
12     num = util.formatNum(num);
13     console.log(num);
14   });
15 </script>
16 </html>
复制代码
复制代码
1 //util
2 define([], function () {
3   return {
4     formatNum: function (n) {
5       if (n < 10) return '0' + n;
6       return n;
7     }
8   };
9 });
复制代码
复制代码
1 //math
2 define(['num'], function (num) {
3   return {
4     getRadom: function () {
5       return parseInt(Math.random() * num);
6     }
7   };
8 });
复制代码
1 //num
2 define([], function () {
3   return 10;
4 });

小结

今天我们实现了一个简单的模块加载器,通过他希望可以帮助各位了解requireJS或者seaJS,最后顺利进入模块化编程的行列

分享到:
评论

相关推荐

    pandas-1.3.5-cp37-cp37m-macosx_10_9_x86_64.zip

    pandas whl安装包,对应各个python版本和系统(具体看资源名字),找准自己对应的下载即可! 下载后解压出来是已.whl为后缀的安装包,进入终端,直接pip install pandas-xxx.whl即可,非常方便。 再也不用担心pip联网下载网络超时,各种安装不成功的问题。

    基于java的大学生兼职信息系统答辩PPT.pptx

    基于java的大学生兼职信息系统答辩PPT.pptx

    基于java的乐校园二手书交易管理系统答辩PPT.pptx

    基于java的乐校园二手书交易管理系统答辩PPT.pptx

    tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl

    tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl

    Android Studio Ladybug(android-studio-2024.2.1.10-mac.zip.002)

    Android Studio Ladybug 2024.2.1(android-studio-2024.2.1.10-mac.dmg)适用于macOS Intel系统,文件使用360压缩软件分割成两个压缩包,必须一起下载使用: part1: https://download.csdn.net/download/weixin_43800734/89954174 part2: https://download.csdn.net/download/weixin_43800734/89954175

    基于ssm框架+mysql+jsp实现的监考安排与查询系统

    有学生和教师两种角色 登录和注册模块 考场信息模块 考试信息模块 点我收藏 功能 监考安排模块 考场类型模块 系统公告模块 个人中心模块: 1、修改个人信息,可以上传图片 2、我的收藏列表 账号管理模块 服务模块 eclipse或者idea 均可以运行 jdk1.8 apache-maven-3.6 mysql5.7及以上 tomcat 8.0及以上版本

    tornado-6.1b2-cp38-cp38-macosx_10_9_x86_64.whl

    tornado-6.1b2-cp38-cp38-macosx_10_9_x86_64.whl

    Android Studio Ladybug(android-studio-2024.2.1.10-mac.zip.001)

    Android Studio Ladybug 2024.2.1(android-studio-2024.2.1.10-mac.dmg)适用于macOS Intel系统,文件使用360压缩软件分割成两个压缩包,必须一起下载使用: part1: https://download.csdn.net/download/weixin_43800734/89954174 part2: https://download.csdn.net/download/weixin_43800734/89954175

    基于MATLAB车牌识别代码实现代码【含界面GUI】.zip

    matlab

    基于java的毕业生就业信息管理系统答辩PPT.pptx

    基于java的毕业生就业信息管理系统答辩PPT.pptx

    基于Web的毕业设计选题系统的设计与实现(springboot+vue+mysql+说明文档).zip

    随着高等教育的普及和毕业设计的日益重要,为了方便教师、学生和管理员进行毕业设计的选题和管理,我们开发了这款基于Web的毕业设计选题系统。 该系统主要包括教师管理、院系管理、学生管理等多个模块。在教师管理模块中,管理员可以新增、删除教师信息,并查看教师的详细资料,方便进行教师资源的分配和管理。院系管理模块则允许管理员对各个院系的信息进行管理和维护,确保信息的准确性和完整性。 学生管理模块是系统的核心之一,它提供了学生选题、任务书管理、开题报告管理、开题成绩管理等功能。学生可以在此模块中进行毕业设计的选题,并上传任务书和开题报告,管理员和教师则可以对学生的报告进行审阅和评分。 此外,系统还具备课题分类管理和课题信息管理功能,方便对毕业设计课题进行分类和归档,提高管理效率。在线留言功能则为学生、教师和管理员提供了一个交流互动的平台,可以就毕业设计相关问题进行讨论和解答。 整个系统设计简洁明了,操作便捷,大大提高了毕业设计的选题和管理效率,为高等教育的发展做出了积极贡献。

    机器学习(预测模型):2000年至2015年期间193个国家的预期寿命和相关健康因素的数据

    这个数据集来自世界卫生组织(WHO),包含了2000年至2015年期间193个国家的预期寿命和相关健康因素的数据。它提供了一个全面的视角,用于分析影响全球人口预期寿命的多种因素。数据集涵盖了从婴儿死亡率、GDP、BMI到免疫接种覆盖率等多个维度,为研究者提供了丰富的信息来探索和预测预期寿命。 该数据集的特点在于其跨国家的比较性,使得研究者能够识别出不同国家之间预期寿命的差异,并分析这些差异背后的原因。数据集包含22个特征列和2938行数据,涉及的变量被分为几个大类:免疫相关因素、死亡因素、经济因素和社会因素。这些数据不仅有助于了解全球健康趋势,还可以辅助制定公共卫生政策和社会福利计划。 数据集的处理包括对缺失值的处理、数据类型转换以及去重等步骤,以确保数据的准确性和可靠性。研究者可以使用这个数据集来探索如教育、健康习惯、生活方式等因素如何影响人们的寿命,以及不同国家的经济发展水平如何与预期寿命相关联。此外,数据集还可以用于预测模型的构建,通过回归分析等统计方法来预测预期寿命。 总的来说,这个数据集是研究全球健康和预期寿命变化的宝贵资源,它不仅提供了历史数据,还为未来的研究和政策制

    基于微信小程序的高校毕业论文管理系统小程序答辩PPT.pptx

    基于微信小程序的高校毕业论文管理系统小程序答辩PPT.pptx

    基于java的超市 Pos 收银管理系统答辩PPT.pptx

    基于java的超市 Pos 收银管理系统答辩PPT.pptx

    基于java的网上报名系统答辩PPT.pptx

    基于java的网上报名系统答辩PPT.pptx

    基于java的网上书城答辩PPT.pptx

    基于java的网上书城答辩PPT.pptx

    婚恋网站 SSM毕业设计 附带论文.zip

    婚恋网站 SSM毕业设计 附带论文 启动教程:https://www.bilibili.com/video/BV1GK1iYyE2B

    基于java的戒烟网站答辩PPT.pptx

    基于java的戒烟网站答辩PPT.pptx

    基于微信小程序的“健康早知道”微信小程序答辩PPT.pptx

    基于微信小程序的“健康早知道”微信小程序答辩PPT.pptx

    机器学习(预测模型):自行车共享使用情况的数据集

    Capital Bikeshare 数据集是一个包含从2020年5月到2024年8月的自行车共享使用情况的数据集。这个数据集记录了华盛顿特区Capital Bikeshare项目中自行车的租赁模式,包括了骑行的持续时间、开始和结束日期时间、起始和结束站点、使用的自行车编号、用户类型(注册会员或临时用户)等信息。这些数据可以帮助分析和预测自行车共享系统的需求模式,以及了解用户行为和偏好。 数据集的特点包括: 时间范围:覆盖了四年多的时间,提供了长期的数据观察。 细节丰富:包含了每次骑行的详细信息,如日期、时间、天气条件、季节等,有助于深入分析。 用户分类:数据中区分了注册用户和临时用户,可以分析不同用户群体的使用习惯。 天气和季节因素:包含了天气情况和季节信息,可以研究这些因素对骑行需求的影响。 通过分析这个数据集,可以得出关于自行车共享使用模式的多种见解,比如一天中不同时间段的使用高峰、不同天气条件下的使用差异、季节性变化对骑行需求的影响等。这些信息对于城市规划者、交通管理者以及自行车共享服务提供商来说都是非常宝贵的,可以帮助他们优化服务、提高效率和满足用户需求。同时,这个数据集也

Global site tag (gtag.js) - Google Analytics