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

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

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

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

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 + 完整定義變量或者函數的表達式vue

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

    錯誤寫法:java

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

    驗證網址,戳我,能夠驗證上述 是否正確;node

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

    // 變量名加花括號
    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 語法糖es6

    使用 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 defaultjson

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

    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()都是有緩存的;

本身總結這篇的時候,查了很多資料,本身作了一些測試,儘可能保證所寫的結論,可以獲得實踐的支撐,以避免貽誤他人;若是文中,有哪些代碼問題,或者邏輯錯誤,歡迎指正!

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

image

參考資料

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

相關文章
相關標籤/搜索