require VS import VS import() 對比分析(史上最詳細)

前置知識-幾種模塊化方案

require 屬於 commonJS 規範,想了解詳細的,戳我;

靜態 import,動態 import() 屬於 ES6 規範;

require 用法

require 可以引用 JS、json 等;

// test.js
module.exports = {
  a: "china",
  b: function() {
    console.log("b");
  }
};

//引用
var test = require("./test");
test.b();

ES6 import

前置知識點

export 用法

export 可以到處變量、對象、函數、類等

  • 寫法 1:export + 完整定義變量或者函數的表達式

    // 變量
    export var a = 10;
    
    // 函數
    export function name() {}
    // 這種寫法要求,必須要是完整定義;比如,變量要有 var、const、let字段,函數要有 function字段
    

    錯誤寫法:

    // 變量 表達式不完整
    var a=10;
    export a;
    
    // 函數 表達式不完整
    var name=function(){};
    export name;
    
    // 對象
    export {
      a:"name"
    }
    
    // 常量
    export 1;
    

    驗證網址,戳我,可以驗證上述 是否正確;

  • 寫法 2:export {變量名}
    針對方法 1 中的錯誤寫法,可以如下改正:

    // 變量名加花括號
    var a = 10;
    export { a };
    
    // 函數名加花括號
    var name = function() {};
    export { name };
    
    // 多個變量時
    export { a, name };
    
    // 一般JS文件,比如utils 文件,在每個方法或者變量前加export,而不是 export {a,name}這種形式;這樣寫的好處,比較方便移植;
    export function name() {}
    export var a = "test";
    
  • 寫法 3:export default 語法糖

    使用 export default 寫法,爲模塊指定默認輸出,這樣就不需要知道所要加載模塊的變量名;export default 在一個 JS 文件中只能使用一次;

    // 變量
    var a = 10;
    export default a;
    
    // 函數
    export default function() {}
    // 等效於
    function fun() {}
    export { fun as default };
    
    // 導出default變量以及其他變量
    export { fun as default, a, name };
    
    // 針對 export { fun as default, a, name };在utils文件中還可以
    export default fun;
    export var a = 0;
    export var name = "china";
    

使用 as 關鍵字

as 關鍵字用來取別名;加入引入多個文件,有相同的變量名,可以使用使用 as 關鍵字避免衝突

// a.js
var a = function() {};
export { a as fun };

// b.js
import { fun as a } from "./a";
a();

靜態 import

  • 引入文件不含有export default

    import { a, b, c } from "./a";
    
  • 引入文件僅含有 export default

    import a from "./d";
    
    //等效於
    import { default as a } from "./d";
    
  • 引入文件既含有export default,還含有其他export

    import $, { each, map } from "jquery";
    

動態 import-import()

import()出現的背景

import 命令會被 JS 引擎靜態分析(編譯階段),先於其他模塊執行;

// 報錯
if (x > 2) {
  import a from "./a";
}
///repl: 'import' and 'export' may only appear at the top level (2:2)

上面代碼中,引擎處理 import 語句是在編譯時,這時不會去分析或執行 if 語句,所以 import 語句放在 if 代碼塊之中毫無意義,因此會報句法錯誤,而不是執行時錯誤。也就是說,import 和 export 命令只能在模塊的頂層,不能在代碼塊之中(比如,在 if 代碼塊之中,或在函數之中)。

這樣的設計,固然有利於編譯器提高效率,但也導致無法在運行時加載模塊。在語法上,條件加載就不可能實現。如果 import 命令要取代 Node 的 require 方法,這就形成了一個障礙。因爲 require 是運行時加載模塊,import 命令無法取代 require 的動態加載功能。

import()特點

特點 1:import() 返回 promise 對象
特點 2:相較於 import 命令,import()可以實現動態加載,其路徑可以根據情況改變
特點 3:import()適用在運行階段,而不是像 import 一樣在編譯階段,這意味着 import(),可以使用在 JS 代碼中,比如條件判斷,函數中等

import()用法

場景 1:按需加載
import()可以在需要的時候,再加載某個模塊。vue 中異步組件的使用,也用到了 import()方法;戳這裏

場景 2:條件加載
import()可以放在 if 代碼塊,根據不同的情況,加載不同的模塊。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

場景 3:動態的模塊路徑

import()允許模塊路徑動態生成。

import(f()).then(...);

注意點:

import()加載模塊成功以後,這個模塊會作爲一個對象,當作 then 方法的參數。因此,可以使用對象解構賦值的語法,獲取輸出接口。

import("./myModule.js").then(({ export1, export2 }) => {
  // ...·
});

上面代碼中,export1 和 export2 都是 myModule.js 的輸出接口,可以解構獲得。

如果模塊有 default 輸出接口,可以用參數直接獲得。

import("./myModule.js").then(myModule => {
  console.log(myModule.default);
});

上面的代碼也可以使用具名輸入的形式。

import("./myModule.js").then(({ default: theDefault }) => {
  console.log(theDefault);
});

如果想同時加載多個模塊,可以採用下面的寫法。

Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});

import()也可以用在 async 函數之中。

async function main() {
  const myModule = await import("./myModule.js");
  const { export1, export2 } = await import("./myModule.js");
  const [module1, module2, module3] = await Promise.all([
    import("./module1.js"),
    import("./module2.js"),
    import("./module3.js")
  ]);
}
main();

緩存

require

  • 第一次加載某個文件時,Node 會緩存該模塊,以後再加載該模塊,就直接從緩存取出該模塊的 module.exports 屬性(不會再執行該模塊)

  • 正常情況下(只有一個 module.exports),require 會緩存(即只執行一次,不會重複執行)

  • 如果需要多次執行模塊中的代碼,一般可以讓模塊暴漏行爲(函數)

  • 如果需要多次執行模塊中的代碼,還可以多次 module.exports

  • 模塊的緩存可以通過 require.cache 拿到,同樣也可以刪除

module.exports 有兩種寫法:

  • module.exports.xxx=abc;// 作爲對象

  • module.exports=abc;// 重新賦值

針對上述結論以及 module.exports 兩種寫法,作了如下測試:

  • demo1
// requie.js module.exports重新賦值 日期
console.log("I am require.js");
module.exports = new Date();

// index.js
const t = require("./require.js");
console.log(t.getTime());

setTimeout(() => {
  const t = require("./require.js");
  console.log(t.getTime());
}, 3000);
console.log("執行頁面");

// 結果:
// I am require.js
// 1576220112564
// 執行頁面
// 1576220112564
  • demo2
// require.js module.exports重新賦值 對象;日期作爲對象屬性值
module.exports = {
  t: new Date()
};

// index.js
const { t } = require("./require.js");
console.log(t.getTime());

setTimeout(() => {
  const { t } = require("./require.js");
  console.log(t.getTime());
}, 3000);

console.log("執行頁面");

// 結果:
// 1576219864761
// 1576219864761
  • demo3
// require.js module.exports重新賦值 對象;日期賦值給變量,作爲函數導出值
let t = new Date();
module.exports = {
  get: () => {
    return t;
  }
};

// index.js
const { get } = require("./require.js");
console.log(get().getTime());

setTimeout(() => {
  const { get } = require("./require.js");
  console.log(get().getTime());
}, 3000);

// 結果:
// 1576219588328
// 1576219588328
  • demo4
// require.js module.exports作爲對象,日期賦值給t屬性
module.exports.t = new Date();
// index.js
const foo = require("./require.js");
console.log(foo.t.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.t.getTime());
}, 3000);

// 結果:
// 1576228870548
// 1576228870548
  • demo5
// require.js module.exports作爲對象;日期作爲對象的值
module.exports.t = {
  time: new Date()
};
// index.js
const foo = require("./require.js");
console.log(foo.t.time.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.t.time.getTime());
}, 3000);

// 結果:
// 1576229080434
// 1576229080434
  • demo6
// require.js 重新賦值對象;日期在函數中運行(暴露函數)
module.exports = {
  get: () => {
    return new Date();
  }
};

// index.js
const { get } = require("./require.js");
console.log(get().getTime());

setTimeout(() => {
  const { get } = require("./require.js");
  console.log(get().getTime());
}, 3000);

// 結果:
// 1576219323353
// 1576219326358

結論:上述情況總結下,require 要想不受緩存影響,把變量等定義在函數中,利用函數延遲執行的特點來解決;

--------兩次導出 module.exports------------

  • demo1
// require.js
module.exports.t = new Date();

setTimeout(() => {
  module.exports.t = new Date();
}, 500);

// index.js
const foo = require("./require.js");
console.log(foo.t.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.t.getTime());
}, 3000);

// 結果:
// 1576225753706
// 1576225754208
  • demo2
// require.js
module.exports = new Date();

setTimeout(() => {
  module.exports = new Date();
}, 500);

// index.js
const foo = require("./require.js");
console.log(foo.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.getTime());
}, 3000);

// 結果:
// 1576226052279
// 1576226052781
  • demo3
// require.js
module.exports = {
  t: new Date()
};

setTimeout(() => {
  module.exports = {
    t: new Date()
  };
}, 500);

// index.js
const foo = require("./require.js");
console.log(foo.t.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.t.getTime());
}, 3000);

// 結果:
// 1576227838138
// 1576227838639

結論,只要是這種 module.exports 導出兩次的,都不會有緩存;

import

  • 靜態 import 只執行一次,不重複執行,有緩存;
  • 動態 import() 只執行一次,不重複執行,有緩存;

針對上述結論,作了如下測試:

  • demo1
// import.js
console.log("import 執行");
export default new Date();
<script type="module">
  import a from "./import/import.js";
  import z from "./import/import.js";
  console.log(a.getTime());
  console.log(z.getTime());

  // 結果:
  // import 執行
  // 1576478875409
  // 1576478875409
</script>
  • demo2
console.log("import 執行");
export default new Date();
<script type="module">
  var b = import("./import/import.js");
  b.then(({ default: time }) => {
    console.log(time.getTime());
  });

  setTimeout(() => {
    var c = import("./import/import.js");
    c.then(({ default: time }) => {
      console.log(time.getTime());
    });
  }, 3000);

  // 結果:
  // 調用文件執行
  // import 執行
  // 1576479834365
  // 1576479834365
</script>

同異步

  • require 是同步加載,先執行加載文件;
  • import()是異步加載,先執行本地執行,然後執行加載文件;

require

// require.js
console.log("I am require.js");
module.exports.t = {
  time: new Date()
};
// index.js
const foo = require("./require.js");
console.log(foo.t.time.getTime());

setTimeout(() => {
  const foo = require("./require.js");
  console.log(foo.t.time.getTime());
}, 3000);

console.log("執行頁面");

// 結果:
// I am require.js
// 1576483718781
// 執行頁面
// 1576483718781

import()

console.log("import 執行");
export default new Date();
<script type="module">
  var b = import("./import/import.js");
  b.then(({ default: time }) => {
    console.log(time.getTime());
  });

  setTimeout(() => {
    var c = import("./import/import.js");
    c.then(({ default: time }) => {
      console.log(time.getTime());
    });
  }, 3000);

  // 結果:
  // 調用文件執行
  // import 執行
  // 1576479834365
  // 1576479834365
</script>

綜述

  • require、動態 import() 運行在 JS 運行階段;靜態 import 運行在編譯階段;
  • require 同步執行,動態 import()異步執行;靜態 import 運行在編譯階段,總是最先執行;
  • require、靜態 import、動態 import()都是有緩存的;

自己總結這篇的時候,查了不少資料,自己做了一些測試,儘量保證所寫的結論,能夠得到實踐的支撐,以免貽誤他人;如果文中,有哪些代碼問題,或者邏輯錯誤,歡迎指正!

本人在日常學習中,也收集了一些有價值的資料,涉及到諸如源碼、設計模式等,歡迎一起交流學習!

微信號

參考資料

es6 import()函數
require、緩存
export MDN
NodeJS 中的 require 和 import
require,import 和 import()函數的區別
require 和 import 的區別是什麼?看這個你就懂了
模塊化
深入理解 ES6 模塊機制
萬歲,瀏覽器原生支持 ES6 export 和 import 模塊啦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章