<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          require加載器實(shí)現(xiàn)原理

          共 14933字,需瀏覽 30分鐘

           ·

          2021-04-27 01:34

          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

          回復(fù)算法,加入前端編程面試算法每日一題群

          來(lái)源:隱冬

          https://juejin.cn/post/6949385808755294245

          我們常說(shuō)node并不是一門(mén)新的編程語(yǔ)言,他只是javascript的運(yùn)行時(shí),運(yùn)行時(shí)你可以簡(jiǎn)單地理解為運(yùn)行javascript的環(huán)境。在大多數(shù)情況下我們會(huì)在瀏覽器中去運(yùn)行javascript,有了node的出現(xiàn),我們可以在node中去運(yùn)行javascript,這意味著哪里安裝了node或者瀏覽器,我們就可以在哪里運(yùn)行javascript。

          1.node模塊化的實(shí)現(xiàn)

          node中是自帶模塊化機(jī)制的,每個(gè)文件就是一個(gè)單獨(dú)的模塊,并且它遵循的是CommonJS規(guī)范,也就是使用require的方式導(dǎo)入模塊,通過(guò)module.export的方式導(dǎo)出模塊。

          node模塊的運(yùn)行機(jī)制也很簡(jiǎn)單,其實(shí)就是在每一個(gè)模塊外層包裹了一層函數(shù),有了函數(shù)的包裹就可以實(shí)現(xiàn)代碼間的作用域隔離。

          你可能會(huì)說(shuō),我在寫(xiě)代碼的時(shí)候并沒(méi)有包裹函數(shù)呀,是的的確如此,這一層函數(shù)是node自動(dòng)幫我們實(shí)現(xiàn)的,我們可以來(lái)測(cè)試一下。

          我們新建一個(gè)js文件,在第一行打印一個(gè)并不存在的變量,比如我們這里打印window,在node中是沒(méi)有window的。

          console.log(window);
          復(fù)制代碼

          通過(guò)node執(zhí)行該文件,會(huì)發(fā)現(xiàn)報(bào)錯(cuò)信息如下。(請(qǐng)使用系統(tǒng)默認(rèn)cmd執(zhí)行命令)。

          (function (exports, require, module, __filename, __dirname) { console.log(window);
          ReferenceError: window is not defined
          at Object.<anonymous> (/Users/choice/Desktop/node/main.js:1:75)
          at Module._compile (internal/modules/cjs/loader.js:689:30)
          at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
          at Module.load (internal/modules/cjs/loader.js:599:32)
          at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
          at Function.Module._load (internal/modules/cjs/loader.js:530:3)
          at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
          at startup (internal/bootstrap/node.js:279:19)
          at bootstrapNodeJSCore (internal/bootstrap/node.js:752:3)
          復(fù)制代碼

          可以看到報(bào)錯(cuò)的頂層有一個(gè)自執(zhí)行的函數(shù),, 函數(shù)中包含exports, require, module, __filename, __dirname這些我們常用的全局變量。

          我在之前的《前端模塊化發(fā)展歷程》一文中介紹過(guò)。自執(zhí)行函數(shù)也是前端模塊化的實(shí)現(xiàn)方案之一,在早期前端沒(méi)有模塊化系統(tǒng)的時(shí)代,自執(zhí)行函數(shù)可以很好的解決命名空間的問(wèn)題,并且模塊依賴的其他模塊都可以通過(guò)參數(shù)傳遞進(jìn)來(lái)。cmd和amd規(guī)范也都是依賴自執(zhí)行函數(shù)實(shí)現(xiàn)的。

          在模塊系統(tǒng)中,每個(gè)文件就是一個(gè)模塊,每個(gè)模塊外面會(huì)自動(dòng)套一個(gè)函數(shù),并且定義了導(dǎo)出方式 module.exports或者exports,同時(shí)也定義了導(dǎo)入方式require。

          let moduleA = (function({
              module.exports = Promise;
              return module.exports;
          })();
          復(fù)制代碼

          2.require加載模塊

          require依賴node中的fs模塊來(lái)加載模塊文件,fs.readFile讀取到的是一個(gè)字符串。

          在javascrpt中我們可以通過(guò)eval或者new Function的方式來(lái)將一個(gè)字符串轉(zhuǎn)換成js代碼來(lái)運(yùn)行。

          • eval
          const name = 'yd';
          const str = 'const a = 123; console.log(name)';
          eval(str); // yd;
          復(fù)制代碼
          • new Function

          new Function接收的是一個(gè)要執(zhí)行的字符串,返回的是一個(gè)新的函數(shù),調(diào)用這個(gè)新的函數(shù)字符串就會(huì)執(zhí)行了。如果這個(gè)函數(shù)需要傳遞參數(shù),可以在new Function的時(shí)候依次傳入?yún)?shù),最后傳入的是要執(zhí)行的字符串。比如這里傳入?yún)?shù)b,要執(zhí)行的字符串str。

          const b = 3;
          const str = 'let a = 1; return a + b';
          const fun = new Function('b', str);
          console.log(fun(b, str)); // 4
          復(fù)制代碼

          可以看到eval和Function實(shí)例化都可以用來(lái)執(zhí)行javascript字符串,似乎他們都可以來(lái)實(shí)現(xiàn)require模塊加載。不過(guò)在node中并沒(méi)有選用他們來(lái)實(shí)現(xiàn)模塊化,原因也很簡(jiǎn)單因?yàn)樗麄兌加幸粋€(gè)致命的問(wèn)題,就是都容易被不屬于他們的變量所影響。

          如下str字符串中并沒(méi)有定義a,但是確可以使用上面定義的a變量,這顯然是不對(duì)的,在模塊化機(jī)制中,str字符串應(yīng)該具有自身獨(dú)立的運(yùn)行空間,自身不存在的變量是不可以直接使用的。

          const a = 1;

          const str = 'console.log(a)';

          eval(str);

          const func = new Function(str);
          func();
          復(fù)制代碼

          node存在一個(gè)vm虛擬環(huán)境的概念,用來(lái)運(yùn)行額外的js文件,他可以保證javascript執(zhí)行的獨(dú)立性,不會(huì)被外部所影響。

          • vm 內(nèi)置模塊

          雖然我們?cè)谕獠慷x了hello,但是str是一個(gè)獨(dú)立的模塊,并不在村hello變量,所以會(huì)直接報(bào)錯(cuò)。

          // 引入vm模塊, 不需要安裝,node 自建模塊
          const vm = require('vm');
          const hello = 'yd';
          const str = 'console.log(hello)';
          wm.runInThisContext(str); // 報(bào)錯(cuò)
          復(fù)制代碼

          所以node執(zhí)行javascript模塊時(shí)可以采用vm來(lái)實(shí)現(xiàn)。就可以保證模塊的獨(dú)立性了。

          3.require代碼實(shí)現(xiàn)

          介紹require代碼實(shí)現(xiàn)之前先來(lái)回顧兩個(gè)node模塊的用法,因?yàn)橄旅鏁?huì)用得到。

          • path模塊

          用于處理文件路徑。

          basename: 基礎(chǔ)路徑, 有文件路徑就不是基礎(chǔ)路徑,基礎(chǔ)路勁是1.js

          extname: 獲取擴(kuò)展名

          dirname: 父級(jí)路勁

          join: 拼接路徑

          resolve: 當(dāng)前文件夾的絕對(duì)路徑,注意使用的時(shí)候不要在結(jié)尾添加/

          __dirname: 當(dāng)前文件所在文件夾的路徑

          __filename: 當(dāng)前文件的絕對(duì)路徑

          const path = require('path''s');
          console.log(path.basename('1.js'));
          console.log(path.extname('2.txt'));
          console.log(path.dirname('2.txt'));
          console.log(path.join('a/b/c''d/e/f')); // a/b/c/d/e/
          console.log(path.resolve('2.txt'));
          復(fù)制代碼
          • fs模塊

          用于操作文件或者文件夾,比如文件的讀寫(xiě),新增,刪除等。常用方法有readFile和readFileSync,分別是異步讀取文件和同步讀取文件。

          const fs = require('fs');
          const buffer = fs.readFileSync('./name.txt''utf8'); // 如果不傳入編碼,出來(lái)的是二進(jìn)制
          console.log(buffer);
          復(fù)制代碼

          fs.access: 判斷是否存在,node10提供的,exists方法已經(jīng)被廢棄, 原因是不符合node規(guī)范,所以我們采用access來(lái)判斷文件是否存在。

          try {
              fs.accessSync('./name.txt');
          catch(e) {
              // 文件不存在
          }
          復(fù)制代碼

          4.手動(dòng)實(shí)現(xiàn)require模塊加載器

          首先導(dǎo)入依賴的模塊path,fs, vm, 并且創(chuàng)建一個(gè)Require函數(shù),這個(gè)函數(shù)接收一個(gè)modulePath參數(shù),表示要導(dǎo)入的文件路徑。

          // 導(dǎo)入依賴
          const path = require('path'); // 路徑操作
          const fs = require('fs'); // 文件讀取
          const vm = require('vm'); // 文件執(zhí)行

          // 定義導(dǎo)入類,參數(shù)為模塊路徑
          function Require(modulePath{
              ...
          }
          復(fù)制代碼

          在Require中獲取到模塊的絕對(duì)路徑,方便使用fs加載模塊,這里讀取模塊內(nèi)容我們使用new Module來(lái)抽象,使用tryModuleLoad來(lái)加載模塊內(nèi)容,Module和tryModuleLoad我們稍后實(shí)現(xiàn),Require的返回值應(yīng)該是模塊的內(nèi)容,也就是module.exports。

          // 定義導(dǎo)入類,參數(shù)為模塊路徑
          function Require(modulePath{
              // 獲取當(dāng)前要加載的絕對(duì)路徑
              let absPathname = path.resolve(__dirname, modulePath);
              // 創(chuàng)建模塊,新建Module實(shí)例
              const module = new Module(absPathname);
              // 加載當(dāng)前模塊
              tryModuleLoad(module);
              // 返回exports對(duì)象
              return module.exports;
          }
          復(fù)制代碼

          Module的實(shí)現(xiàn)很簡(jiǎn)單,就是給模塊創(chuàng)建一個(gè)exports對(duì)象,tryModuleLoad執(zhí)行的時(shí)候?qū)?nèi)容加入到exports中,id就是模塊的絕對(duì)路徑。

          // 定義模塊, 添加文件id標(biāo)識(shí)和exports屬性
          function Module(id{
              this.id = id;
              // 讀取到的文件內(nèi)容會(huì)放在exports中
              this.exports = {};
          }
          復(fù)制代碼

          之前我們說(shuō)過(guò)node模塊是運(yùn)行在一個(gè)函數(shù)中,這里我們給Module掛載靜態(tài)屬性wrapper,里面定義一下這個(gè)函數(shù)的字符串,wrapper是一個(gè)數(shù)組,數(shù)組的第一個(gè)元素就是函數(shù)的參數(shù)部分,其中有exports,module. Require,__dirname, __filename, 都是我們模塊中常用的全局變量。注意這里傳入的Require參數(shù)是我們自己定義的Require。

          第二個(gè)參數(shù)就是函數(shù)的結(jié)束部分。兩部分都是字符串,使用的時(shí)候我們將他們包裹在模塊的字符串外部就可以了。

          Module.wrapper = [
              "(function(exports, module, Require, __dirname, __filename) {",
              "})"
          ]

          復(fù)制代碼

          _extensions用于針對(duì)不同的模塊擴(kuò)展名使用不同的加載方式,比如JSON和javascript加載方式肯定是不同的。JSON使用JSON.parse來(lái)運(yùn)行。

          javascript使用vm.runInThisContext來(lái)運(yùn)行,可以看到fs.readFileSync傳入的是module.id也就是我們Module定義時(shí)候id存儲(chǔ)的是模塊的絕對(duì)路徑,讀取到的content是一個(gè)字符串,我們使用Module.wrapper來(lái)包裹一下就相當(dāng)于在這個(gè)模塊外部又包裹了一個(gè)函數(shù),也就實(shí)現(xiàn)了私有作用域。

          使用call來(lái)執(zhí)行fn函數(shù),第一個(gè)參數(shù)改變運(yùn)行的this我們傳入module.exports,后面的參數(shù)就是函數(shù)外面包裹參數(shù)exports, module, Require, __dirname, __filename

          Module._extensions = {
              '.js'(module) {
                  const content = fs.readFileSync(module.id, 'utf8');
                  const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
                  const fn = vm.runInThisContext(fnStr);
                  fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
              },
              '.json'(module) {
                  const json = fs.readFileSync(module.id, 'utf8');
                  module.exports = JSON.parse(json); // 把文件的結(jié)果放在exports屬性上
              }
          }
          復(fù)制代碼

          tryModuleLoad函數(shù)接收的是模塊對(duì)象,通過(guò)path.extname來(lái)獲取模塊的后綴名,然后使用Module._extensions來(lái)加載模塊。

          // 定義模塊加載方法
          function tryModuleLoad(module{
              // 獲取擴(kuò)展名
              const extension = path.extname(module.id);
              // 通過(guò)后綴加載當(dāng)前模塊
              Module._extensions[extension](module);
          }
          復(fù)制代碼

          至此Require加載機(jī)制我們基本就寫(xiě)完了,我們來(lái)重新看一下。Require加載模塊的時(shí)候傳入模塊名稱,在Require方法中使用path.resolve(__dirname, modulePath)獲取到文件的絕對(duì)路徑。然后通過(guò)new Module實(shí)例化的方式創(chuàng)建module對(duì)象,將模塊的絕對(duì)路徑存儲(chǔ)在module的id屬性中,在module中創(chuàng)建exports屬性為一個(gè)json對(duì)象。

          使用tryModuleLoad方法去加載模塊,tryModuleLoad中使用path.extname獲取到文件的擴(kuò)展名,然后根據(jù)擴(kuò)展名來(lái)執(zhí)行對(duì)應(yīng)的模塊加載機(jī)制。

          最終將加載到的模塊掛載module.exports中。tryModuleLoad執(zhí)行完畢之后module.exports已經(jīng)存在了,直接返回就可以了。

          // 導(dǎo)入依賴
          const path = require('path'); // 路徑操作
          const fs = require('fs'); // 文件讀取
          const vm = require('vm'); // 文件執(zhí)行

          // 定義導(dǎo)入類,參數(shù)為模塊路徑
          function Require(modulePath{
              // 獲取當(dāng)前要加載的絕對(duì)路徑
              let absPathname = path.resolve(__dirname, modulePath);
              // 創(chuàng)建模塊,新建Module實(shí)例
              const module = new Module(absPathname);
              // 加載當(dāng)前模塊
              tryModuleLoad(module);
              // 返回exports對(duì)象
              return module.exports;
          }
          // 定義模塊, 添加文件id標(biāo)識(shí)和exports屬性
          function Module(id{
              this.id = id;
              // 讀取到的文件內(nèi)容會(huì)放在exports中
              this.exports = {};
          }
          // 定義包裹模塊內(nèi)容的函數(shù)
          Module.wrapper = [
              "(function(exports, module, Require, __dirname, __filename) {",
              "})"
          ]
          // 定義擴(kuò)展名,不同的擴(kuò)展名,加載方式不同,實(shí)現(xiàn)js和json
          Module._extensions = {
              '.js'(module) {
                  const content = fs.readFileSync(module.id, 'utf8');
                  const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
                  const fn = vm.runInThisContext(fnStr);
                  fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
              },
              '.json'(module) {
                  const json = fs.readFileSync(module.id, 'utf8');
                  module.exports = JSON.parse(json); // 把文件的結(jié)果放在exports屬性上
              }
          }
          // 定義模塊加載方法
          function tryModuleLoad(module{
              // 獲取擴(kuò)展名
              const extension = path.extname(module.id);
              // 通過(guò)后綴加載當(dāng)前模塊
              Module._extensions[extension](module);
          }
          復(fù)制代碼

          5.給模塊添加緩存

          添加緩存也比較簡(jiǎn)單,就是文件加載的時(shí)候?qū)⑽募湃刖彺嬖?,再去加載模塊時(shí)先看緩存中是否存在,如果存在直接使用,如果不存在再去重新嘉愛(ài),加載之后再放入緩存。

          // 定義導(dǎo)入類,參數(shù)為模塊路徑
          function Require(modulePath{
              // 獲取當(dāng)前要加載的絕對(duì)路徑
              let absPathname = path.resolve(__dirname, modulePath);
              // 從緩存中讀取,如果存在,直接返回結(jié)果
              if (Module._cache[absPathname]) {
                  return Module._cache[absPathname].exports;
              }
              // 嘗試加載當(dāng)前模塊
              tryModuleLoad(module);
              // 創(chuàng)建模塊,新建Module實(shí)例
              const module = new Module(absPathname);
              // 添加緩存
              Module._cache[absPathname] = module;
              // 加載當(dāng)前模塊
              tryModuleLoad(module);
              // 返回exports對(duì)象
              return module.exports;
          }
          復(fù)制代碼

          6.自動(dòng)補(bǔ)全路徑

          自動(dòng)給模塊添加后綴名,實(shí)現(xiàn)省略后綴名加載模塊,其實(shí)也就是如果文件沒(méi)有后綴名的時(shí)候遍歷一下所有的后綴名看一下文件是否存在。

          // 定義導(dǎo)入類,參數(shù)為模塊路徑
          function Require(modulePath{
              // 獲取當(dāng)前要加載的絕對(duì)路徑
              let absPathname = path.resolve(__dirname, modulePath);
              // 獲取所有后綴名
              const extNames = Object.keys(Module._extensions);
              let index = 0;
              // 存儲(chǔ)原始文件路徑
              const oldPath = absPathname;
              function findExt(absPathname{
                  if (index === extNames.length) {
                     return throw new Error('文件不存在');
                  }
                  try {
                      fs.accessSync(absPathname);
                      return absPathname;
                  } catch(e) {
                      const ext = extNames[index++];
                      findExt(oldPath + ext);
                  }
              }
              // 遞歸追加后綴名,判斷文件是否存在
              absPathname = findExt(absPathname);
              // 從緩存中讀取,如果存在,直接返回結(jié)果
              if (Module._cache[absPathname]) {
                  return Module._cache[absPathname].exports;
              }
              // 嘗試加載當(dāng)前模塊
              tryModuleLoad(module);
              // 創(chuàng)建模塊,新建Module實(shí)例
              const module = new Module(absPathname);
              // 添加緩存
              Module._cache[absPathname] = module;
              // 加載當(dāng)前模塊
              tryModuleLoad(module);
              // 返回exports對(duì)象
              return module.exports;
          }
          復(fù)制代碼

          7.分析實(shí)現(xiàn)步驟

          • 1.導(dǎo)入相關(guān)模塊,創(chuàng)建一個(gè)Require方法。

          • 2.抽離通過(guò)Module._load方法,用于加載模塊。

          • 3.Module.resolveFilename 根據(jù)相對(duì)路徑,轉(zhuǎn)換成絕對(duì)路徑。

          • 4.緩存模塊 Module._cache,同一個(gè)模塊不要重復(fù)加載,提升性能。

          • 5.創(chuàng)建模塊 id: 保存的內(nèi)容是 exports = {}相當(dāng)于this。

          • 6.利用tryModuleLoad(module, filename) 嘗試加載模塊。

          • 7.Module._extensions使用讀取文件。

          • 8.Module.wrap: 把讀取到的js包裹一個(gè)函數(shù)。

          • 9.將拿到的字符串使用runInThisContext運(yùn)行字符串。

          • 10.讓字符串執(zhí)行并將this改編成exports。

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持




          瀏覽 39
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美操逼的网站亚洲 | 免费精品视频 | 久久99精品久久久久久不卡l中文无码精品 | 一区二区无码在线 | 免费看操|