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


Posted in Javascript onJanuary 15, 2018

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

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

② 解决前端多人协作问题

③ 解决文件依赖问题

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

⑤ ......

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

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

加载器原理分析

分与合

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

//求得绩效系数
 var performanceCoefficient = function () {
  return 0.2;
 };

 //住房公积金计算方式
 var companyReserve = function (salary) {
  return salary * 0.2;
 };

 //个人所得税
 var incomeTax = function (salary) {
  return salary * 0.2;
 };

 //基本工资
 var salary = 1000;

 //最终工资
 var mySalary = salary + salary * performanceCoefficient();
 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
 console.log(mySalary);

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

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

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

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

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

<script src="companyReserve.js" type="text/javascript"></script>
<script src="incomeTax.js" type="text/javascript"></script>
<script src="performanceCoefficient.js" type="text/javascript"></script>
<script type="text/javascript">

 //基本工资
 var salary = 1000;

 //最终工资
 var mySalary = salary + salary * performanceCoefficient();
 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
 console.log(mySalary);

</script>

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

require与define

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

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

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

companyReserve = requile('companyReserve');

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

require.config({
 'companyReserve': 'companyReserve',
 'incomeTax': 'incomeTax',
 'performanceCoefficient': 'performanceCoefficient'
});

require(['companyReserve', 'incomeTax', 'performanceCoefficient'], function (companyReserve, incomeTax, performanceCoefficient) {
 //基本工资
 var salary = 1000;

 //最终工资
 var mySalary = salary + salary * performanceCoefficient();
 mySalary = mySalary - companyReserve(mySalary) - incomeTax(mySalary - companyReserve(mySalary));
 console.log(mySalary);
});

这里便是一个标准的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,模块的构造函数

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

(function () {

 var Module = function () {
  this.status = 'loading'; //只具有loading与loaded两个状态
  this.depCount = 0; //模块依赖项
  this.value = null; //define函数回调执行的返回
 };


 var loadScript = function (url, callback) {

 };

 var config = function () {

 };

 var require = function (deps, callback) {

 };

 require.config = function (cfg) {

 };

 var define = function (deps, callback) {

 };

})();

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

代码实现

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

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

(function () {

 //存储已经加载好的模块
 var moduleCache = {};

 var require = function (deps, callback) {
  var params = [];
  var depCount = 0;
  var i, len, isEmpty = false, modName;

  //获取当前正在执行的js代码段,这个在onLoad事件之前执行
  modName = document.currentScript && document.currentScript.id || 'REQUIRE_MAIN';

  //简单实现,这里未做参数检查,只考虑数组的情况
  if (deps.length) {
   for (i = 0, len = deps.length; i < len; i++) {
    (function (i) {
     //依赖加一
     depCount++;
     //这块回调很关键
     loadMod(deps[i], function (param) {
      params[i] = param;
      depCount--;
      if (depCount == 0) {
       saveModule(modName, params, callback);
      }
     });
    })(i);
   }
  } else {
   isEmpty = true;
  }

  if (isEmpty) {
   setTimeout(function () {
    saveModule(modName, null, callback);
   }, 0);
  }

 };

 //考虑最简单逻辑即可
 var _getPathUrl = function (modName) {
  var url = modName;
  //不严谨
  if (url.indexOf('.js') == -1) url = url + '.js';
  return url;
 };

 //模块加载
 var loadMod = function (modName, callback) {
  var url = _getPathUrl(modName), fs, mod;

  //如果该模块已经被加载
  if (moduleCache[modName]) {
   mod = moduleCache[modName];
   if (mod.status == 'loaded') {
    setTimeout(callback(this.params), 0);
   } else {
    //如果未到加载状态直接往onLoad插入值,在依赖项加载好后会解除依赖
    mod.onload.push(callback);
   }
  } else {

   /*
   这里重点说一下Module对象
   status代表模块状态
   onLoad事实上对应requireJS的事件回调,该模块被引用多少次变化执行多少次回调,通知被依赖项解除依赖
   */
   mod = moduleCache[modName] = {
    modName: modName,
    status: 'loading',
    export: null,
    onload: [callback]
   };

   _script = document.createElement('script');
   _script.id = modName;
   _script.type = 'text/javascript';
   _script.charset = 'utf-8';
   _script.async = true;
   _script.src = url;

   //这段代码在这个场景中意义不大,注释了
   //   _script.onload = function (e) {};

   fs = document.getElementsByTagName('script')[0];
   fs.parentNode.insertBefore(_script, fs);

  }
 };

 var saveModule = function (modName, params, callback) {
  var mod, fn;

  if (moduleCache.hasOwnProperty(modName)) {
   mod = moduleCache[modName];
   mod.status = 'loaded';
   //输出项
   mod.export = callback ? callback(params) : null;

   //解除父类依赖,这里事实上使用事件监听较好
   while (fn = mod.onload.shift()) {
    fn(mod.export);
   }
  } else {
   callback && callback.apply(window, params);
  }
 };

 window.require = require;
 window.define = require;

})();

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

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

未处理循环依赖问题

未处理CMD写法

未处理html模板加载相关

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

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

......

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

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

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

document.currentScript

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

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

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

demo实现

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
 <title></title>
</head>
<body>
</body>
<script src="require.js" type="text/javascript"></script>
<script type="text/javascript">
 require(['util', 'math', 'num'], function (util, math, num) {

  num = math.getRadom() + '_' + num;
  num = util.formatNum(num);
  console.log(num);
 });
</script>
</html>
//util
define([], function () {
 return {
  formatNum: function (n) {
   if (n < 10) return '0' + n;
   return n;
  }
 };
});
//math
define(['num'], function (num) {
 return {
  getRadom: function () {
   return parseInt(Math.random() * num);
  }
 };
});
//math
define(['num'], function (num) {
 return {
  getRadom: function () {
   return parseInt(Math.random() * num);
  }
 };
});

小结

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
图片连续滚动代码[兼容IE/firefox]
Jun 11 Javascript
javascript showModalDialog 多层模态窗口实现页面提交及刷新的代码
Nov 28 Javascript
AJAX 网页保留浏览器前进后退等功能
Feb 12 Javascript
Jsonp 跨域的原理以及Jquery的解决方案
Jun 27 Javascript
购物车选中得到价格实现示例
Jan 26 Javascript
node.js中的fs.fsyncSync方法使用说明
Dec 15 Javascript
javascript中sort()的用法实例分析
Jan 30 Javascript
js前端解决跨域问题的8种方案(最新最全)
Nov 18 Javascript
微信小程序微信支付接入开发实例详解
Apr 12 Javascript
vuex存储token示例
Nov 11 Javascript
d3.js 地铁轨道交通项目实战
Nov 27 Javascript
vue实现购物车功能(商品分类)
Apr 20 Javascript
vue2.0 如何把子组件的数据传给父组件(推荐)
Jan 15 #Javascript
利用Angular2 + Ionic3开发IOS应用实例教程
Jan 15 #Javascript
js实现一个简单的MVVM框架示例
Jan 15 #Javascript
详解angularjs 学习之 scope作用域
Jan 15 #Javascript
高性能的javascript之加载顺序与执行原理篇
Jan 14 #Javascript
关于axios如何全局注册浅析
Jan 14 #Javascript
Vue+Flask实现简单的登录验证跳转的示例代码
Jan 13 #Javascript
You might like
用Zend Studio+PHPnow+Zend Debugger搭建PHP服务器调试环境步骤
2014/01/19 PHP
PHP的preg_match匹配字符串长度问题解决方法
2014/05/03 PHP
PHP的文件操作与算法实现的面试题示例
2015/08/10 PHP
PHP用FTP类上传文件视频等的简单实现方法
2016/09/23 PHP
PHP用mysql_insert_id()函数获得刚插入数据或当前发布文章的ID
2016/11/25 PHP
tp5修改(实现即点即改)
2019/10/18 PHP
laravel orm 关联条件查询代码
2019/10/21 PHP
window.showModalDialog使用手册
2007/01/11 Javascript
jQuery对表单元素的取值和赋值操作代码
2011/05/19 Javascript
Moment.js 不容错过的超棒Javascript日期处理类库
2012/04/15 Javascript
JavaScript将Table导出到Excel实现思路及代码
2013/03/13 Javascript
MyEclipse取消验证Js的两种方法
2013/11/14 Javascript
Javascript writable特性介绍
2015/02/27 Javascript
js+html5实现canvas绘制简单矩形的方法
2015/06/05 Javascript
JS常见简单正则表达式验证功能小结【手机,地址,企业税号,金额,身份证等】
2017/01/22 Javascript
JavaScript实现公历转农历功能示例
2017/02/13 Javascript
深入理解AngularJs-scope的脏检查(一)
2017/06/19 Javascript
Layui实现带查询条件的分页
2019/07/27 Javascript
原生js实现自定义滚动条
2021/01/20 Javascript
Python守护进程用法实例分析
2015/06/04 Python
Python的组合模式与责任链模式编程示例
2016/02/02 Python
Python中的字符串操作和编码Unicode详解
2017/01/18 Python
Python 含参构造函数实例详解
2017/05/25 Python
tensorflow如何批量读取图片
2019/08/29 Python
Python 实现使用空值进行赋值 None
2020/03/12 Python
解决Python paramiko 模块远程执行ssh 命令 nohup 不生效的问题
2020/07/14 Python
浅谈Python __init__.py的作用
2020/10/28 Python
35款精致的 CSS3 和 HTML5 网页模板 推荐
2012/08/03 HTML / CSS
微软瑞士官方网站:Microsoft瑞士
2018/04/20 全球购物
荷兰家电购物网站:Expert.nl
2020/01/18 全球购物
Linux文件操作命令都有哪些
2015/02/27 面试题
生产操作工岗位职责
2014/09/16 职场文书
升学宴学生致辞
2015/07/27 职场文书
学生会部长竞选稿
2015/11/19 职场文书
环境保护宣传标语大全!
2019/06/28 职场文书
你为什么是穷人?可能是这5个缺点造成
2019/07/11 职场文书