深入理解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 相关文章推荐
JS控制表格隔行变色
Jun 26 Javascript
读jQuery之十一 添加事件核心方法
Jul 31 Javascript
关于js注册事件的常用方法
Apr 03 Javascript
深入理解Javascript动态方法调用与参数修改的问题
Dec 10 Javascript
JQEasy-ui在IE9以下版本中二次加载的问题分析及处理方法
Jun 23 Javascript
JavaScript中具名函数的多种调用方式总结
Nov 08 Javascript
JQuery中解决重复动画的方法
Oct 17 Javascript
通过示例彻底搞懂js闭包
Aug 10 Javascript
微信小程序实现自定义picker选择器弹窗内容
May 26 Javascript
浅谈angular2子组件的事件传递(任意组件事件传递)
Sep 30 Javascript
echarts实现词云自定义形状的示例代码
Feb 20 Javascript
解决layui批量传值到后台操作时出现传值为空的问题
Sep 28 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
PHP file_get_contents 函数超时的几种解决方法
2009/07/30 PHP
用PHP实现 上一篇、下一篇的代码
2012/09/29 PHP
CI框架学习笔记(二) -入口文件index.php
2014/10/27 PHP
php以fastCGI的方式运行时文件系统权限问题及解决方法
2015/05/11 PHP
php封装的mysqli类完整实例
2016/10/18 PHP
PHP实现动态创建XML文档的方法
2018/03/30 PHP
FormValidate 表单验证功能代码更新并提供下载
2008/08/23 Javascript
javascript form 验证函数 弹出对话框形式
2009/06/23 Javascript
网页右下角弹出窗体实现代码
2014/06/05 Javascript
JavaScript+html5 canvas制作色彩斑斓的正方形效果
2016/01/27 Javascript
学习Bootstrap滚动监听 附调用方法
2016/07/02 Javascript
JavaScript实现前端分页控件
2017/04/19 Javascript
Node.js实现连接mysql数据库功能示例
2017/09/15 Javascript
jQuery实现checkbox全选、反选及删除等操作的方法详解
2019/08/02 jQuery
在Vue中使用HOC模式的实现
2020/08/23 Javascript
[01:43]3.19DOTA2发布会 三代刀塔人第三代
2014/03/25 DOTA
[03:38]TI4西雅图DOTA2前线报道 71专访
2014/07/08 DOTA
[54:18]DOTA2-DPC中国联赛 正赛 PSG.LGD vs LBZS BO3 第一场 1月22日
2021/03/11 DOTA
windows下python模拟鼠标点击和键盘输示例
2014/02/28 Python
python机器学习理论与实战(四)逻辑回归
2018/01/19 Python
Django框架使用富文本编辑器Uedit的方法分析
2018/07/31 Python
使用Python实现租车计费系统的两种方法
2018/09/29 Python
python中将正则过滤的内容输出写入到文件中的实例
2018/10/21 Python
python每5分钟从kafka中提取数据的例子
2019/12/23 Python
Python如何使用bokeh包和geojson数据绘制地图
2020/03/21 Python
Python中的全局变量如何理解
2020/06/04 Python
理解Django 中Call Stack机制的小Demo
2020/09/01 Python
CSS3新属性transition-property transform box-shadow实例学习
2013/06/06 HTML / CSS
美国宠物护理专家:Revival Animal Health
2020/01/05 全球购物
机械绘图员岗位职责
2013/11/19 职场文书
预防煤气中毒方案
2014/06/16 职场文书
会计学习心得体会
2014/09/09 职场文书
无刑事犯罪记录证明
2014/09/18 职场文书
教育项目合作协议书格式
2014/10/17 职场文书
2015年六一儿童节演讲稿
2015/03/19 职场文书
python文件目录操作之os模块
2021/05/08 Python