深入理解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 相关文章推荐
Jquery写一个鼠标拖动效果实现原理与代码
Dec 24 Javascript
Extjs 3.3切换tab隐藏相应工具栏出现空白解决
Apr 02 Javascript
jquery自定义属性(类型/属性值)
May 21 Javascript
js格式化金额可选是否带千分位以及保留精度
Jan 28 Javascript
javascript中interval与setTimeOut的区别示例介绍
Mar 14 Javascript
js判断字符长度及中英文数字等
Mar 19 Javascript
jQuery使用fadein方法实现渐出效果实例
Mar 27 Javascript
jQuery插件FusionCharts绘制的2D双柱状图效果示例【附demo源码】
May 13 jQuery
微信小程序实现的自定义分享功能示例
Feb 12 Javascript
Vue 指令实现按钮级别权限管理功能
Apr 23 Javascript
微信小程序 简易计算器实现代码实例
Sep 02 Javascript
使用JS监听键盘按下事件(keydown event)
Nov 07 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
浅谈电磁辐射对健康的影响
2021/03/01 无线电
php下判断数组中是否存在相同的值array_unique
2008/03/25 PHP
php array_slice函数的使用以及参数详解
2008/08/30 PHP
PHP 事务处理数据实现代码
2010/05/13 PHP
php一个找二层目录的小东东
2012/08/02 PHP
php中利用explode函数分割字符串到数组
2014/02/08 PHP
Joomla使用Apache重写模式的方法
2016/05/04 PHP
PHPUnit测试私有属性和方法功能示例
2018/06/12 PHP
thinkphp5.1框架容器与依赖注入实例分析
2019/07/23 PHP
jQuery 学习 几种常用方法
2009/06/11 Javascript
extjs 列表框(multiselect)的动态添加列表项的方法
2009/07/31 Javascript
使用jquery实现IE下按backspace相当于返回操作
2014/03/18 Javascript
页面元素绑定jquery toggle后元素隐藏的解决方法
2014/03/27 Javascript
js数组的操作指南
2014/12/28 Javascript
jQuery实现图片文字淡入淡出效果
2015/12/21 Javascript
JavaScript数据结构与算法之二叉树遍历算法详解【先序、中序、后序】
2019/02/21 Javascript
nodejs微信开发之自动回复的实现
2019/03/17 NodeJs
webpack4手动搭建Vue开发环境实现todoList项目的方法
2019/05/16 Javascript
微信小程序多列表渲染数据开关互不影响的实现
2020/06/05 Javascript
为react组件库添加typescript类型提示的方法
2020/06/15 Javascript
jQuery实现本地存储
2020/12/22 jQuery
[02:57]DOTA2英雄基础教程 风行者
2014/01/16 DOTA
python中numpy包使用教程之数组和相关操作详解
2017/07/30 Python
python实现kMeans算法
2017/12/21 Python
python 实现得到当前时间偏移day天后的日期方法
2018/12/31 Python
Python 私有化操作实例分析
2019/11/21 Python
基于python实现复制文件并重命名
2020/09/16 Python
移动web模拟客户端实现多方框输入密码效果【附代码】
2016/03/25 HTML / CSS
美国学校用品、教室和教学商店:Discount School Supply
2018/04/04 全球购物
澳大利亚在线家具店:Luxo Living
2019/03/24 全球购物
企业党员公开承诺书
2014/03/26 职场文书
初中国旗下的演讲稿
2014/08/28 职场文书
工作检讨书大全
2015/01/26 职场文书
聚众斗殴罪辩护词
2015/05/21 职场文书
Django与数据库交互的实现
2021/06/03 Python
「Manga Time Kirara MAX」2022年5月号封面公开
2022/03/21 日漫