模块加载

js的标准规范有以下几种 CommandJSES ModuleAMDCMDUMD

CommandJS

CommandJS是服务端JS的标准规范,特点是只能用于服务端。

核心语法为require引入和module.exports输出。

每一个文件是一个模块,有自己的作用域。在文件内定义的变量、函数、类都是私有的,对其他文件不可见。

每个模块内部,module变量代表当前模块,该变量是一个对象。他有一个exports属性,这个属性是对外的接口。加载某一个模块,其实就是加载该模块的module.exports属性。

特点:

  • 所有代码运行在模块作用域内,不会污染全局变量
  • 模块可以加载多次,但是只有第一次加载时运行一次。然后运行结果就被缓存下来,以后再加载,就是直接读取缓存的结果。

使用(Node环境)

nodejs里的规范,环境变量:moduleexportsrequireglobal

声明及引用

// 声明
module.exports.add = function(){}
// 引用
const math = require('./math')
math.add()

// 声明
module.exports = function(){}
// 引用
const math = require('./math')
math()

// 声明
module.exports = { huang: 'huang', jin: 'jin' }
// 引用
const { huang, jin } = require('./math')

module

node内部提供一个Module构建函数。每一个模块内部都有一个module对象,代表当前模块。它有以下属性

  • id 模块标识符,通常是带有绝对路径的模块文件名

  • path 模块的文件名,绝对路径

  • exports 模块对外输出的值,其他文件加载该模块其实就是读取module.exports变量

  • parent 调用该模块的模块

  • children 该模块用到的其他模块

  • loaded 该模块是否已经加载完(在父模块中require一个子模块之后,子模块的loaded才变为true)

循环依赖

Node的循环依赖处理方式是:如果有循环,执行到哪就算哪。简单来说就是,当A模块开始加载时,缓存中会立刻出现A模块的module.exports,当require(b),并且b中require(a)的时候,b只能获取到循环依赖之前的a。

//a.js
module.exports.max = 10
require(b)
module.exports.max = 20
//b.js
const a = require(a)
console.log(a.max) // 10

加载模式

加载模块的方式是同步的。在输入时先加载整个模块,生成一个对象。再从这个对象上读取方法,这种加载被称为运行时加载。只有对应子模块加载完成,才能执行后面的操作。 为什么是同步的?因为Nodejs是用于服务端编程,模块文件存在于硬盘中,读取非常快。

加载时机

输入的值是被输出的值的拷贝。 父模块引入了一个子模块,其实引入的是这个子模块输出的值的拷贝,一旦输出了这个值,模块内部的变化就影响不到这个值了。

ES6 Module

异步执行,模块几乎同时导入,后面模块不需要等待前面模块导入完成。ES6 Module 输出的是值的动态引用,不会缓存。

ES6 在语言标准层面上,实现了模块功能,而且实现的非常简单,宗旨是在浏览器和服务器通用的模块解决方案。

ES6 Module中使用import引入,export输出。

特点:

  • import 是只读属性,不能赋值。相当于const
  • export/import 提升,import/export必须位于模块的顶级,不可以位于作用域内,其次对于模块内的import/export都会提升到模块的顶部。

使用

声明及引用

// 声明
export function add(){}
import { add } from './math'

export const name = 'box'
import { name } from './math'

// 引用math模块中所有方法并存放在module变量中。
export function add(){}
export const name = 'box'
import * as module from './math'

// 为模块指定默认输出,一个文件中只能有一个export default,且后面不能跟变量声明的语句
// 本质上,export default就是输出一个叫default的变量或者方法,然后系统允许你为它重命名。
export default { add } 
import add from './math' // export default 声明 引用

function add(){}
export {add as default}; // 等同于export default add
import {default as foo} from './module'; // 等同于import foo from './module'

// export 与 import 的复合写法
export {foo,bar} from 'module';
// 等同于
import {foo,bar} from 'module;
export {foo,bar}
// 上面代码中,export和import语句可以结合为一行代码。但是,写成一行以后,foo和bar实际上没有被导入当前模块,只是相当于当前对外转发了这两个接口,导致当前模块不能直接使用foo,bar。



import()函数

import的模块需要静态分析,所以不能用于动态加载。也就不能完成required同样的功能,因此,引入了import()函数,返回一个Promise对象。

这个函数的引入起到的很好的作用,比如我们在做多语言加载的时候,我们需要引入语言包,但是我们又不想一次性将所有语言包全部引入,我们只需要引入需要的语言包就可以了,那么就用到了import()函数。

import (path).then(res=>{
    console.log(res)
}).catch(err=>{
    console.log(res)
})

环境加载

传统方法

script标签默认是同步加载的,加上deferasync就会开启异步加载。

区别:

defer要等到整个页面在内存中正常渲染结束,才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。另外,如果有多个defer脚本,会按照他们在页面中出现的顺序加载,而多个async脚本,是不能保证按顺序加载

es6模块加载

浏览器加载es6模块,也是用 <script>标签,不过要加入 type="module" 属性。添加该属性后,默认开启 defer 属性。若想开启 async 属性,可以直接添加。作用同上。

<script type="module">
    import {add, redis} from './module';
</script>

node.js加载

node中原本存在的commonJSes6的模块加载并不兼容。因此node中做了限制

.mjs 文件总是以es6模块加载; .cjs 文件总是以commonJS加载, .js 文件的加载取决于 package.json 中type字段,若 type="module" 则以es6模块加载,默认commonJS

循环依赖

ES6 Module其实并不关心有没有循环依赖,他并不需要产生结果,他只需要给你一个引用即可,至于是否能取到值,那么就需要开发者自己来保证了。

加载时机

import 是静态命令的方式,js引擎对脚本静态分析时,遇到模块加载命令import,会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被记载的那么模块中去取值。模块内部引用的变化会反应在外部。

import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为编译时加载。在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

AMD

AMDRequireJS使用的规范,是为了浏览器中的模块加载而实现的。浏览器环境要加载资源,需要从服务器端加载模块,依靠网络下载,时间比较长。所以需要采用非同步的模块。AMD是异步加载。而AMD的设计思路,也是参考了一部分CommandJS的。

AMD相关的api define,用于定义模块,如果我们定义的模块本身也依赖其他模块,那么就需要把它放在数组中,作为第一个参数

使用

// 声明
define(['jquery', 'underscore'], function ($, _) {
    //    methods
    function a(){};    //    private because it's not returned (see below)
    function b(){};    //    public because it's returned
    function c(){};    //    public because it's returned

    //    exposed public methods
    return {
        b: b,
        c: c
    }
});
// math.js 只有在有依赖的情况下,才需要进行定义依赖,否则可以直接传入内容,比如:
define(function(){
    return {
        add:function(){}
    }
})
require(['math'], function(math) {
    console.log(math.add());
})

循环依赖

强制忽略,比如有两个模块A,B。当A依赖B,然后B依赖A的时候,B获取到的A是为未定义的状态。而且总是会把依赖的模块执行完成,也就是说B一定会被先执行完成。

CMD

CMD sea.js它的各个方面和AMD都非常像,只不过CMD推崇依赖就近 延迟加载AMD推崇的是前置依赖

define(function(require,exports,module){
    const a = require('a')
    a.x()
})

但是也是延迟执行,还是要等加载完。判断方式就是正则啦。

Function.prototype.toString.call(function(){
    console.log('i am Magic')
})

UMD

UMD是一个为了解决跨平台MD导入问题(是一种思想),它的解决方法就是通过揉合CommandJSAMD CMD来解决这个问题。

先判断是否支持Nodejs模块(exports是否存在),如果存在就使用Nodejs模块。不支持的话,再判断是否支持AMD/CMD(判断define是否存在)。都不行就挂载在window全局对象上

// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
  }
}(this, function () {

    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

其实它就是自定义了一种包装方式,并且对每种环境下进行不同处理。如果是有依赖的情况,一样会进行特殊情况处理。

对比

*AMDCommandJSUMDCMDES Module
使用在浏览器
使用在服务端
异步加载✅(允许)✅(允许)
最后更新时间:
贡献者: DESKTOP-ER5718D\zt