首頁  >  文章  >  web前端  >  requireJS如何實作模組載入器?

requireJS如何實作模組載入器?

亚连
亚连原創
2018-06-11 17:54:391455瀏覽

本篇文章主要介紹了深入理解requireJS-實作一個簡單的模組載入器,現在分享給大家,也給大家做個參考。

在前文中我們不只一次強調模組化程式設計的重要性,以及其可以解決的問題:

① 解決單一檔案變數命名衝突問題

② 解決前端多人協作問題

③ 解決檔案依賴問題

④ 按需載入(這個說法其實很假了)

⑤ ......

為了深入了解載入器,中間閱讀過一點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行程式碼

於是我們這邊便會開始分:

#

<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 = {
 &#39;companyReserve&#39;: &#39;companyReserve&#39;,
 &#39;incomeTax&#39;: &#39;incomeTax&#39;,
 &#39;performanceCoefficient&#39;: &#39;performanceCoefficient&#39;
};

於是我們一個模組便對應了一個路徑js文件,剩下的便是將之對應模組的載入了,因為前端模組涉及到請求。所以這種寫法:

companyReserve = requile(&#39;companyReserve&#39;);

對於前端來說是不適用的,就算你在哪裡看到這樣做了,也一定是其中做了一些“手腳”,這裡我們便需要依據AMD規範了:

require.config({
 &#39;companyReserve&#39;: &#39;companyReserve&#39;,
 &#39;incomeTax&#39;: &#39;incomeTax&#39;,
 &#39;performanceCoefficient&#39;: &#39;performanceCoefficient&#39;
});
require([&#39;companyReserve&#39;, &#39;incomeTax&#39;, &#39;performanceCoefficient&#39;], 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 = &#39;loading&#39;; //只具有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 || &#39;REQUIRE_MAIN&#39;;
  //简单实现,这里未做参数检查,只考虑数组的情况
  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(&#39;.js&#39;) == -1) url = url + &#39;.js&#39;;
  return url;
 };
 //模块加载
 var loadMod = function (modName, callback) {
  var url = _getPathUrl(modName), fs, mod;
  //如果该模块已经被加载
  if (moduleCache[modName]) {
   mod = moduleCache[modName];
   if (mod.status == &#39;loaded&#39;) {
    setTimeout(callback(this.params), 0);
   } else {
    //如果未到加载状态直接往onLoad插入值,在依赖项加载好后会解除依赖
    mod.onload.push(callback);
   }
  } else {
   /*
   这里重点说一下Module对象
   status代表模块状态
   onLoad事实上对应requireJS的事件回调,该模块被引用多少次变化执行多少次回调,通知被依赖项解除依赖
   */
   mod = moduleCache[modName] = {
    modName: modName,
    status: &#39;loading&#39;,
    export: null,
    onload: [callback]
   };
   _script = document.createElement(&#39;script&#39;);
   _script.id = modName;
   _script.type = &#39;text/javascript&#39;;
   _script.charset = &#39;utf-8&#39;;
   _script.async = true;
   _script.src = url;
   //这段代码在这个场景中意义不大,注释了
   //   _script.onload = function (e) {};
   fs = document.getElementsByTagName(&#39;script&#39;)[0];
   fs.parentNode.insertBefore(_script, fs);
  }
 };
 var saveModule = function (modName, params, callback) {
  var mod, fn;
  if (moduleCache.hasOwnProperty(modName)) {
   mod = moduleCache[modName];
   mod.status = &#39;loaded&#39;;
   //输出项
   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([&#39;util&#39;, &#39;math&#39;, &#39;num&#39;], function (util, math, num) {
  num = math.getRadom() + &#39;_&#39; + num;
  num = util.formatNum(num);
  console.log(num);
 });
</script>
</html>
//util
define([], function () {
 return {
  formatNum: function (n) {
   if (n < 10) return &#39;0&#39; + n;
   return n;
  }
 };
});
//math
define([&#39;num&#39;], function (num) {
 return {
  getRadom: function () {
   return parseInt(Math.random() * num);
  }
 };
});
//math
define([&#39;num&#39;], function (num) {
 return {
  getRadom: function () {
   return parseInt(Math.random() * num);
  }
 };
});

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

有关在Vue2.x中父组件与子组件双向绑定(详细教程)

详细介绍在Vue2.0中v-for迭代语法的变化(详细教程)

在vue2.0中循环遍历并且加载不同图片(详细教程)

以上是requireJS如何實作模組載入器?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn