JavaScript模块化原理深入分析

 更新时间:2022年11月25日 15:29:38   作者:我不吃饼干  
JavaScript中的模块化是指将每个js文件会被认为单独一个的模块。模块之间是互相不可见的。如果一个模块需要使用另一个模块,那么需要通过指定语法来引入要使用的模块,而且只能使用引入模块所暴露的内容

1. 为什么需要 Javascipt 模块化

  • 解决命名冲突。将所有变量都挂载在到全局 global 会引用命名冲突的问题。模块化可以把变量封装在模块内部。
  • 解决依赖管理。Javascipt 文件如果存在相互依赖的情况就需要保证被依赖的文件先被加载。使用模块化则无需考虑文件加载顺序。
  • 按需加载。如果引用 Javascipt 文件较多,同时加载会花费加多时间。使用模块化可以在文件被依赖的时候被加载,而不是进入页面统一加载。
  • 代码封装。将相同功能代码封装起来方便后续维护和复用。

2. 你知道哪几种模块化规范

CommonJS

Node.js 采用了 CommonJS 模块规范。

CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。使用 require 方法加载模块。模块加载的顺序,按照其在代码中出现的顺序。

模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

引入模块得到的值其实是模块输出值的拷贝,如果是复杂对象则为浅拷贝。

// a.js
let count = 1;
function inc() {
    count++;
}
module.exports = {
    count: count,
    inc: inc
};
// b.js
const a = require('./a.js');
console.log(a.count); // 1
a.inc();
console.log(a.count); // 1

因为 CommonJS 输出的是值的浅拷贝,也就是说 count 在输出后就不再和原模块的 count 有关联。

在 Node 中每一个模块都是一个对象,其有一个 exports 属性,就是文件中指定的 module.exports,当我们通过 require 获取模块时,得到的就是 exports 属性。再看另一个例子:

// a.js
module.exports = 123;
setTimeout(() => {
    module.exports = 456;
}, 1000);
// b.js
console.log(require('./a.js')); // 123
setTimeout(() => {
    console.log(require('./a.js')); // 456
}, 2000);

模块的 module.exports 值改变了,我们通过 require 获取模块的值也会发生变化。

CommonJS 使用了同步加载,即加载完成后才进行后面的操作,所以比较适合服务端,如果用在浏览器则可能导致页面假死。

AMD

AMD(Asynchronous Module Definition,异步加载模块定义)。这里异步指的是不堵塞浏览器其他任务(dom构建,css渲染等),而加载内部是同步的(加载完模块后立即执行回调)。 AMD 也采用 require 命令加载模块,但是不同于 CommonJS,它要求两个参数,依赖模块和回调:

require([module], callback);

以 RequireJS 示例, 具体语法可以参考 requirejs.org/

简单提供一下代码示例,方便后续理解。

定义两个模块 calclog 模块

// calc.js
define(function(require, factory) {
    function add(...args) {
        return args.reduce((prev, curr) => prev + curr, 0);
    }
    return {
        add
    }
});
// log.js
define(function(require, factory) {
    function log(...args) {
        console.log('---log.js---');
        console.log(...args)
    }
    return log
});

index.js 中引用两个模块

require(['./calc.js', './log.js'], function (calc, log) {
    log(calc.add(1,2,3,4,5));
});

在 HTML 中引用

<script src="./require.js"></script>
<script src="./index.js"></script>

可以看到在被依赖模块加载完成后会把返回值作为依赖模块的参数传入,在被加载模块全部执行完成后可以去执行加载模块。

UMD

UMD(Universal Module Definition,通用模块定义),所谓的通用,就是兼容了 CommonJS 和 AMD 规范,这意味着无论是在 CommonJS 规范的项目中,还是 AMD 规范的项目中,都可以直接引用 UMD 规范的模块使用。

原理其实就是在模块中去判断全局是否存在 exportsdefine,如果存在 exports,那么以 CommonJS 的方式暴露模块,如果存在 define 那么以 AMD 的方式暴露模块:

(function (root, factory) {
  if (typeof define === "function" && define.amd) {
    define(["jquery", "underscore"], factory);
  } else if (typeof exports === "object") {
    module.exports = factory(require("jquery"), require("underscore"));
  } else {
    root.Requester = factory(root.$, root._);
  }
}(this, function ($, _) {
  // this is where I defined my module implementation
  const Requester = { // ... };
  return Requester;
}));

ESM (ES6 模块)

CommonJS 和 AMD 模块,都只能在运行时确定输入输出,而 ES6 模块是在编译时就能确定模块的输入输出,模块的依赖关系。

在 Node.js 中使用 ES6 模块需要在 package.json 中指定 {"type": "module"}

在浏览器环境使用 ES6 模块需要指定 <script type="module" src="module.js"></script>

ES6 模块通过 importexport 进行导入导出。ES6 模块中 import 的值是原始值的动态只读引用,即原始值发生变化,引用值也会变化。

import 命令具有提升效果,会提升到整个模块的头部,优先执行。

// a.js
export const obj = {
    a: 5
}
// b.js
console.log(obj)
import { obj } from './a.js'
// 运行 b.js 输出: { a: 5 }

importexport 指定必须处理模块顶层,也就是说不能在 iffor 等语句内。下面这种使用方式是不合法的。

if (expr) {
    import val from 'some_module'; // error!
}

UMD 通常是在 ESM 不起作用情况下备用,未来趋势是浏览器和服务器都会支持 ESM。

由于 ES6 模块是在编译阶段执行的,可以更好的在编译阶段进行代码优化,如 Tree Shaking 就是依赖 ES6 模块去静态分析代码而删除无用代码。

3. CommonJS 和 ES6 模块的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJs 是单个值导出,ES6 Module可以导出多个
  • CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  • CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined

4. CommonJS 和 AMD 实现原理

CommonJS

我们通过写一个简单的 demo 实现 CommonJS 来理解其原理。

1、实现文件的加载和执行

我们在用 Node.js 时都知道有几个变量和函数是不需要引入可以直接使用的,就是 require()__filename__dirnameexportsmodule。这些变量都是 Node.js 在执行文件时注入进去的。

举个栗子,我们创建一个 add.js 文件,导出一个 add() 函数:

function add(a, b) {
    return a + b;
}
module.exports = add;

现在我们要加载并执行这个文件,我们可以通过 fs.readFileSync 加载文件。

const fs = require("fs");
// 同步读取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件内容

我们要在执行时传入 require()__filename__dirnameexportsmodule 这几个参数,可以在一个函数中执行这段代码,而函数的参数就是这几个参数即可。我们简单的创建一个函数,函数的内容就是刚才我们加载的文件内容,参数名依次是规范要求注入的几个参数。

// 通过 new Function 生成函数,参数分别是函数的入参和函数的内容
const compiledWrapper = new Function(
    "exports",
    "require",
    "module",
    "__filename",
    "__dirname",
    data
);

现在我们执行这个函数,先不考虑 require__filename__dirname,只传 exportsmodule

const mymodule = {};
const myexports = (mymodule.exports = {});
// 执行函数并传入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null);

现在我们可以简单的了解导出变量的原理,我们把 module 传给函数,在函数中,把需要导出的内容挂在 module 上,我们就可以通过 module 获取导出内容了。

exports 只是 module.exports 的一个引用,我们可以给 module.exports 赋值,也可以通过 exports.xxx 形式赋值,这样也相当于给 module.exports.xxx 赋值。但是如果直接给 exports 赋值将不生效,因为这样 exports 就和 module 没关系了,我们本质上还是要把导出结果赋值给 module.exports

现在的完整代码贴一下:

const fs = require("fs");
// 同步读取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件内容
// 创建函数
const compiledWrapper = new Function(
    "exports",
    "require",
    "module",
    "__filename",
    "__dirname",
    data
);
const mymodule = {};
const myexports = (mymodule.exports = {});
// 执行函数并传入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null);
console.log(mymodule, myexports, mymodule.exports(1, 2));
// { exports: [Function: add] } {} 3

我们可以获取了 add 函数,并成功调用。

2、引用文件

我们刚才已经成功加载并执行了文件,如何在另一个文件通过 require 引用呢。其实就是把上面的操作封装一下。

不过现在我们把参数全部传进去,require__filename__dirname,分别是我们当前实现的 require 函数,加载文件的文件路径,加载文件的目录路径。

const fs = require('fs');
const path = require('path');
function _require(filename) {
  // 同步读取文件
  const data = fs.readFileSync(filename, 'utf8'); // 文件内容
  const compiledWrapper = new Function(
    'exports',
    'require',
    'module',
    '__filename',
    '__dirname',
    data
  );
  const mymodule = {};
  const myexports = (mymodule.exports = {});
  const _filename = path.resolve(filename)
  const _dirname = path.dirname(_filename);
  compiledWrapper.call(myexports, _require, myexports, mymodule, _filename, _dirname);
  return mymodule.exports
}
const add = _require('./add.js')
console.log(add(12, 13)); // 25

3、模块缓存

现在就实现了文件的加载和引用,现在还差一点,就是缓存。之前说过,一个模块只会加载一次,然后在全局缓存起来,所以需要在全局保存缓存对象。

// add.js
console.log('[add.js] 加载文件....')
function add(a, b) {
  return a + b;
}
module.exports = add;
// require.js
const fs = require('fs');
const path = require('path');
// 把缓存对象原型设置为null 防止通过原型链查到同名的key (比如一个模块叫 toString
const _cache = Object.create(null);
function _require(filename) {
  const cachedModule = _cache[filename];
  if (cachedModule) {
    // 如果存在缓存就直接返回
    return cachedModule.exports;
  }
  // 同步读取文件
  const data = fs.readFileSync(filename, 'utf8'); // 文件内容
  const compiledWrapper = new Function(
    'exports',
    'require',
    'module',
    '__filename',
    '__dirname',
    data
  );
  const mymodule = {};
  const myexports = (mymodule.exports = {});
  const _filename = path.resolve(filename);
  const _dirname = path.dirname(_filename);
  compiledWrapper.call(
    myexports,
    _require,
    myexports,
    mymodule,
    _filename,
    _dirname
  );
  _cache[filename] = mymodule;
  return mymodule.exports;
}
const add1 = _require('./add.js');
const add2 = _require('./add.js');
console.log(add1(12, 13)); // [add.js] 加载文件.... 25
console.log(add2(13, 14)); // 27

可以看到加了缓存后,引用了两次模块,但只加载了一次。

一个简单的 CommonJS 规范实现就完成了。

AMD

上面提供了 RequireJS 的示例代码,打开控制台可以发现 HTML 中被添加了两个 <script> 标签,引入了程序中依赖的两个文件。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Document</title>
<script type="text/javascript" charset="utf-8" async data-requirecontext="_"data-requiremodule="A" src="././calc.js "></script>
<script type="text/javascript" charset="utf-8" async data-requirecontext="_"data-requiremodule="B" src="././log.js "></script>
</head>
<body> == $0
<script src=" . /require.js"></script>
<script src=" . / index.js"></script>
</body>
</html>

这样我们可以推测 RequireJS 的实现原理,就是在执行程序的过程中,发现依赖文件未被引用,就在 HTML 中插入一个 <script> 节点引入文件。

这里涉及一个知识点,我们可以看到被 RequireJS 插入的标签都设置了 async 属性。

  • 如果我们直接使用 script 脚本的话,HTML 会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的 DOM 渲染。
  • 如果设置了 async,脚本会异步加载,并在加载完成后立即执行。
  • 如果设置了 defer,浏览器会异步的下载文件并且不会影响到后续 DOM 的渲染,在文档渲染完毕后,DOMContentLoaded 事件调用前执行,按照顺序执行所有脚本。

所以我们可以推测 RequireJS 原理,通过引入 <script> 标签异步加载依赖文件,等依赖文件全部加载完成,把文件的输入作为参数传入依赖文件。

到此这篇关于JavaScript模块化原理深入分析的文章就介绍到这了,更多相关JS模块化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • js如何实现淡入淡出效果

    js如何实现淡入淡出效果

    这篇文章主要介绍了原生js如何实现淡入淡出效果,文章为大家提供了一个已经封装好的代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2015-11-11
  • 纯js实现隔行变色效果

    纯js实现隔行变色效果

    这篇文章主要为大家详细介绍了纯js实现隔行变色效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • 关于对TypeScript泛型参数的默认值理解

    关于对TypeScript泛型参数的默认值理解

    泛型可以理解为宽泛的类型,通常用于类和函数,下面这篇文章主要给大家介绍了关于对TypeScript泛型参数默认值的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • JavaScript实现动态数据可视化的示例详解

    JavaScript实现动态数据可视化的示例详解

    动态数据可视化能够将大量数据以直观、生动的方式呈现,帮助用户更好地理解和分析数据,本文主要为大家介绍了如何使用JavaScript实现这一功能,需要的可以参考下
    2024-02-02
  • Jquery对数组的操作技巧整理

    Jquery对数组的操作技巧整理

    这篇文章主要介绍了Jquery对数组的操作技巧,需要的朋友可以参考下
    2014-03-03
  • 微信小程序实现上传视频功能

    微信小程序实现上传视频功能

    这篇文章主要为大家详细介绍了微信小程序实现上传视频功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • IE7中javascript操作CheckBox的checked=true不打勾的解决方法

    IE7中javascript操作CheckBox的checked=true不打勾的解决方法

    在IE7下,生成的Checkbox无法正确的打上勾。 原因是 chkbox控件还没初始化(appendChild),就开始操作它的结果
    2009-12-12
  • JavaScript中的冒泡排序法

    JavaScript中的冒泡排序法

    这篇文章主要介绍了JavaScript中的冒泡排序法的知识,并通过一个例子给大家讲解了js冒泡排序,非常不错,具有参考借鉴价值,感兴趣的朋友一起学习吧
    2016-08-08
  • JavaScript必备的断点调试技巧总结(推荐)

    JavaScript必备的断点调试技巧总结(推荐)

    打断点操作很简单,核心的问题在于,断点怎么打才能够排查出代码的问题所在呢?下面这篇文章主要给大家总结介绍了关于JavaScript必备的断点调试技巧,需要的朋友可以参考下
    2021-09-09
  • javascript轮播图算法

    javascript轮播图算法

    这篇文章主要为大家详细介绍了javascript轮播图算法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10

最新评论