11.模块管理规范(ES6、CommonsJs、AMD、CMD)

目录


Vue专栏目录(点击进入…)



模块管理规范(ES6、CommonsJs、AMD、CMD)

模块化其实是一种规范、一种约束,这种约束会大大提升开发效率。将每个js文件看作是一个模块,每个模块通过固定的方式引入,并且通过固定的方式向外暴露指定的内容。按照js模块化的设想,一个个模块按照其依赖关系组合,最终插入到主程序中
ES6、CommonJS、AMD、CMD指的都是一种规范


原始JavaScript使用方法:<script>标签
最原始的JavaScript文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在window对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口。

原始的加载方式暴露了一些显而易见的弊端
(1)全局作用域下容易造成变量冲突
(2)文件只能按照<script>的书写顺序进行加载
(3)开发人员必须主观解决模块和代码库的依赖关系
(4)在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪


(1)CommonJS:同步加载

CommonJS定义的是模块的同步加载,是一个更偏向于服务器端的规范(也可以在浏览器中使用),主要用于Nodejs,根据CommonJS规范,一个单独的文件就是一个模块,加载模块使用require()方法,该方法读取一个文件并执行,最后返回文件内部的exports对象

所有代码都运行在模块作用域,不会污染全局作用域;模块可以多次加载,但只会在第一次加载的时候运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果;模块的加载顺序,按照代码的出现顺序是同步加载的

在使用Node开发程序的时候,这个程序由很多个模块组成,每一个模块都是一个文件(单独作用域),在该模块内部定义的变量其他模块没有办法读取的到,除非定义为global对象的属性。Node模块采用的就是CommonJS规范,该规范的核心思想是允许模块通过 require() 方法来同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口

输出模块:每个模块只有一个出口,module.export对象,把想要输出的模块放入里面
加载模块:加载一个模块的


CommonJS模块化特点:

①:所有代码都运行在模块作用域,不会污染全局作用域
②:模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
③:npm中已经有将近20万个可以使用模块包


CommonJS模块化缺点:

①:同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载
②:浏览器资源是异步加载的不能非阻塞的并行加载多个模块

CommonJs所加载的模块一般都已经存在本地的硬盘里面,当执行某个文件的时候会一同加载文件里面require()所引入的文件,加载起来非常快,不要考虑异步加载的方式。但是这个只是在于服务器端,如果是浏览器端,要从服务器加载模块就必须采用异步的方式,于是乎就有了AMD CMD的解决方案


模块上下文(Module Context)

(1)在模块中,有一个自由变量“require”,即一个函数
        ①:“require”功能接受模块标识符
        ②:“require”返回外部模块的导出的API
        ③:如果存在依赖周期,则外部模块在其传递依赖之一要求时可能尚未完成执行。在这种情况下,由“require”返回的对象必须至少包含外部模块在调用要求导致当前模块执行之前准备的导出
        ④:如果无法返回所请求的模块,则“require”必须引发错误
(2)在模块中,有一个名为“exports”的自由变量,该变量是模块执行时可以向其添加API的对象
(3)模块必须使用“exports”对象作为唯一的导出方法

CommonJS中规定每个文件是一个模块。每个模块是拥有各自的作用域的,各自作用域的变量互不影响


CommonJS模块化导出(★)

下面两种写法实质上是一样的

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

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

其内在机制是将exports指向了module.exports,而module.exports在初始化时是一个空对象


可以简单地理解为,CommonJS在每个模块的首部默认添加了以下代码:

var module = {
	exports: {},
};
var exports = module.exports;

因此,为exports.add赋值相当于在module.exports对象上添加了一个add属性


CommonJS模块化导出注意点

(1)注意点一:不要直接给exports赋值,否则会导致其失效

exports = {
	name: 'calculater'
};

由于对exports进行了赋值操作,使其指向了新的对象{name: ‘calculater’},module.exports却仍然是原来的空对象,因此name属性并不会被导出


(2)注意点二:不要把module.exports与exports混用

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

上面的代码先通过exports导出了add属性,相当于module.exports = { add: function(){…}}然后将module.exports重新赋值为另外一个对象。这会导致原本拥有add属性的对象丢失了,最后导出的只有name


(3)注意点三:导出语句不代表模块的末尾

module.exports = {
	name: 'lcylcy'
};

console.log('end');

module.exports或exports后面的代码依旧会照常执行。比如上面的console会在控制台上打出“end”,但在实际使用中,为了提高可读性,不建议采取上面的写法,而是应该将module.exports及exports语句放在模块的末尾


CommonJS模块化导入(★)

在CommonJS中使用require进行模块导入


common.js

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

index.js

const calculator = require('./common.js');
const sum = calculator.add(2, 3);
console.log(sum); // 5

在index.js中导入了common模块,并调用了它的add函数。当require一个模块时会有两种情况:
(1)require的模块是第一次被加载。这时会首先执行该模块,然后导出内容
(2)require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果


CommonJS模块化导出、导入例子:

common.js

console.log('running common.js');
// 导出
module.exports = {
    name: 'common',
    add: function(a, b) {
        return a + b;
    }
};

index.js

// 导入common.js中的add方法
const add = require('./common.js').add;
const sum = add(2, 3);
console.log('sum:', sum);

// 导入common.js中的name属性
const moduleName = require('./common.js').name;
console.log('end');

控制台的输出结果如下:

running calculator.js
sum: 5
end

从结果可以看到,require两次引用了common.js,但“console.log(‘running common.js’);”只执行了一遍。模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded用于记录该模块是否被加载过。它的值默认为false,当模块第一次被加载和执行过后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码


加载整个模块
有时加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require即可

require('./test.js');

require函数接收表达式
另外,require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径

const moduleNames = ['foo.js', 'bar.js'];
moduleNames.forEach(name => {
    require('./' + name);
});

CommonJS实例

math.js

// 导出add函数
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};

increment.js

// 从math.js导入add函数
var add = require('math').add;

// 导出increment 函数
exports.increment = function(val) {
    return add(val, 1);
};

program.js

// 从increment.js导入add函数
var inc = require('increment').increment;
var a = 1;
// 调用
inc(a);   // 2

(2)AMD:异步加载

Asynchronous Module Definition(异步模块定义)。它是一个在浏览器端模块化开发的规范。它不是JavaScript原生支持,所以使用AMD规范进行页面开发需要用到对应的库,也就是RequireJS,AMD规范会提前加载依赖模块,AMD规范是通过requireJs在推广过程中对模块定义的规范化产出

AMD规范: https://github.com/amdjs/amdjs-api/wiki/AMD
AMD定义模块异步加载适用于浏览器端,为了JavaScript的模块化开发


为什要有异步加载?

浏览器如果使用common.js同步加载模块的话,就会导致性能等问题,所以针对这个问题,又出了一个规范,这个规范可以实现异步加载依赖模块

AMD实现方案:先定义所有依赖,然后在加载完成后的回调函数中执行
AMD是预加载,只有一个接口:

define(id?,dependencies?,factory); 

它要在声明模块的时候制定所有的依赖(dep),并且还要当做形参传到factory中
注意:这些依赖文件并没有书写顺序的区别

但是开始就把所有依赖写出来是不符合书写的逻辑顺序的,能不能像commonJS那样用的时候再require,而且还支持异步加载后再执行呢?


AMD优缺点

优点:

①:适合在浏览器环境中异步加载模块
②:可以并行加载多个模块


缺点:

①:提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
②:不符合通用的模块化思维方式,是一种妥协的实现


(3)CMD:异步加载

Common Module Definition(CMD), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。CMD推崇as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行,延迟加载依赖模块。CMD规范是SeaJs在推广过程中对模块定义的规范化产出

CMD规范: https://github.com/seajs/seajs/issues/242

CMD优缺点

优点:

①:依赖就近,延迟执行
②:可以很容易在 Node.js 中运行


缺点

①:依赖SPM打包,模块的加载逻辑偏重


(4)ES6

ES6 Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。import和export也作为保留关键字在ES6版本中加入了进来(CommonJS中的module并不属于关键字)

ES6静态加载的设计思想,使得在编译时就可以确定模块的依赖关系,以及输入、输出的变量。ES6则在语言层面上实现了模块化,取代CommonJS、AMD、CMD成为服务端和浏览器端通用的模块解决方案(CommonJS、AMD、CMD运行时确定依赖关系)


ES6模块化特点

ES6 Module是ES6中规定的模块体系,相比上面提到的规范,ES6 Module有更多的优势,ES6模块化的特点:
静态加载,编译时确定依赖关系
(1)自动运行于严格模式之下
(2)export关键字导出
(3)import关键字导入
(4)同步、异步加载均可


ES6优缺点

优点:

(1)容易进行静态分析
(2)面向未来的 EcmaScript 标准


缺点:

(1)原生浏览器端还没有实现该标准
(2)全新的命令字,新版的 Node.js才支持
(3)目前浏览器对ES6 Module兼容还不太好,平时在webpack中使用的export/import,会经过babel转换为CommonJS规范


对CommonJS改进

将前面的common.js和index.js使用ES6的方式进行了改写

common.js

export default {
    name: 'common',
    add: function(a, b) {
        return a + b;
    }
};

index.js

import calculator from './common.js';

const sum = calculator.add(2, 3);
console.log(sum); // 5

ES6 Module会自动采用严格模式,这在ES5(ECMAScript 5.0)中是一个可选项。以前可以通过选择是否在文件开始时加上“use strict”来控制严格模式,在ES6 Module中不管开头是否有“use strict”,都会采用严格模式。如果将原本是CommonJS的模块或任何未开启严格模式的代码改写为ES6 Module要注意这点


ES6 Module导出(3种)

在ES6 Module中使用export命令来导出模块

注意:使用export指令导出模块对外提供接口后,就可以通过import命令来加载对应的模块了,首先需要在html代码中引入js文件,并且<script>的类型需要设置为module

<script src="*.js" type="module"></script>
function sum(sum1,sum2){
	return sum1 + sum2;
}

export var name = "张三"
export { sum }
export function mul(sum1,sum2){
	return sum1 +sum2;
}
// 导出类
export class Run{
	constructor(){}
	run(){
		consloe.log("导出类")
	}
}
// 默认导出的东西,在使用import导入的时候可以随意命名,一个js文件仅有一个
export default const name = "曹孟德"

1.命名导出

一个模块可以有多个命名导出。它有两种不同的写法:

// 写法1:将变量的声明和导出写在一行
export const name = 'calculator';
export const add = function(a, b) { return a + b; };
// 写法2:先进行变量声明,然后再用同一个export语句导出
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };

两种写法的效果是一样的。

在使用命名导出时,可以通过as关键字对变量重命名。如:

const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add as getSum }; // 在导入时即为 name 和 getSum

2.默认导出

与命名导出不同,模块的默认导出只能有一个。如:

export default {
    name: 'calculator',
    add: function(a, b) {
        return a + b;
    }
};

可以将export default理解为对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可

// 导出字符串
export default 'This is calculator.js';
// 导出匿名函数
export default function() {...}
// 导出 class
export default class {...}

3.全部导出

function A1(){}
function A2(){}
function A3(){}

方式一:
const All = {
	A1,A2,A3
}
方式二:
export default All

ES6 Module导入

ES6 Module中使用import语法导入模块。加载带有命名导出的模块

项目说明
import导入的库对象的变量名
from可以写具体的路径,也可以写包名;如果写的是包名就会去/node_modules中查找
// 导入*.js定义的变量、函数等,{}中命名必须和export中一致
import { flag , sum} from "*.js"
// 导入所有
import * as all from "*.js"

1.命名导入

common.js

const name = 'common';
const add = function(a, b) { return a + b; };
export { name, add };

index.js

import { name, add } from './common.js';
add(2, 3);

加载带有命名导出的模块时,那就要对应命名导入。import后面要跟{ }来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。

导入变量的效果相当于在当前作用域下声明了这些变量(name和add),并且不可对其进行更改,也就是所有导入的变量都是只读的。

与命名导出类似,我们可以通过as关键字可以对导入的变量重命名。如:

import { name, add as commonSum } from './common.js';
commonSum(2, 3);

在导入多个变量时,还可以采用整体导入的方式。如:

import * as common from './common.js';
console.log(common.add(2, 3));
console.log(common.name);

使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中,从而减少了对当前作用域的影响


2.默认导入

common.js

export default {
    name: 'common',
    add: function(a, b) { return a + b; }
};

index.js

import myCommon from './common.js';
myCommon.add(2, 3);

对于默认导出来说,那就要默认导入,import后面直接跟变量名,并且这个名字可以自由指定(比如这里是myCommon),它指代了common.js中默认导出的值。

从原理上可以这样去理解:

import { default as myCommon } from './common.js';

3.混合导入

index.js

import React, { Component } from 'react';

这里的React对应的是该模块的默认导出,而Component则是其命名导出中的一个变量。
注意:这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误


4.复合写法

复合写法在工程中,有时需要把某一个模块导入之后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:

export { name, add } from './common.js';

等同于

import { name, add } from './common.js';
export { name, add };

复合写法目前只支持当被导入模块(这里的common.js)通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆开写。

import common from "./common.js ";
export default common;

不能写成export default from ‘./common.js’,除非写为

export { default } from common;

但是这种方式依然还是命名导出而不是默认导出,命名的变量为default而已


5.全部导入

通过“*”可导入模块中所有的export变量,通常情况下需要给“*”起一个别名,方便使用

//可以部分导入,节省体积
//B1 as MyB1:B1导入同时重命名为MyB1
import {B1 as MyB1,B2} from './module/moduleA'
consloe.log(MyB1)

AMD规范和CMD规范区别

1.对于依赖的模块

AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不同)


2.CMD推崇依赖就近,AMD推崇依赖前置

AMD的API默认是一个当多个用,CMD的API严格区分,推崇职责单一。比如AMD里,require 分全局require和局部require,都叫require。CMD里,没有全局require,而是根据模块系统的完备性,提供seajs.use来实现模块系统的加载启动。CMD里,每个API都简单纯粹


ES6 Module和CommonJS的区别

CommonJSES6 Module
模块输出是值的拷贝模块输出是值的引用(引用时可能修改到模块的值,被输出模块的内部的改变会影响引用的改变
模块是运行时加载模块是编译时加载
加载的是整个模块,将所有的接口全部加载进来可以加载全部,也可以单独加载其中的某个接口
this指向当前模块this指向undefined

动态与静态

CommonJSES6 Module
动态静态

动态与静态CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”
“动态”含义:模块依赖关系的建立发生在代码运行阶段
“静态”含义:模块依赖关系的建立发生在代码编译阶段


CommonJS的例子:

// common.js
module.exports = { name: 'common' };

// index.js
const name = require('./common.js').name;

在上面介绍CommonJS的部分时提到过,当模块A加载模块B时(在上面的例子中是index.js加载calculator.js),会执行B中的代码,并将其module.exports对象作为require函数的返回值进行返回。并且require的模块路径可以动态指定,支持传入一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段


ES6 Module的写法:

// common.js
export const name = 'common';

// index.js
import { name } from './common.js';

ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句中)。因此,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系


ES6 Module相比于CommonJS来说具备以下几点优势:
(1)死代码检测和排除
可以用静态分析工具检测出哪些模块没有被调用过
比如:在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积

(2)模块变量类型检查
JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的

(3)编译器优化
在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高


值拷贝与动态映射

在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的


什么是CommonJS中的值拷贝?

// calculator.js
var count = 0;
module.exports = {
    count: count,
    add: function(a, b) {
        count += 1;
        return a + b;
    }
};

// index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;

console.log(count); // 0(这里的count是对 calculator.js 中 count 值的拷贝)
add(2, 3);
console.log(count); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)

count += 1;
console.log(count); // 1(拷贝的值可以更改)

index.js中的count是对calculator.js中count的一份值拷贝,因此在调用add函数时,虽然更改了原本calculator.js中count的值,但是并不会对index.js中导入时创建的副本造成影响。

另一方面,在CommonJS中允许对导入的值进行更改。我们可以在index.js更改count和add,将其赋予新值。同样,由于是值的拷贝,这些操作不会影响calculator.js本身


使用ES6 Module将上面的例子进行改写:

// calculator.js
let count = 0;
const add = function(a, b) {
    count += 1;
    return a + b;
};
export { count, add };

// index.js
import { count, add } from './calculator.js';

console.log(count); // 0(对 calculator.js 中 count 值的映射)
add(2, 3);
console.log(count); // 1(实时反映calculator.js 中 count值的变化)

// count += 1; // 不可更改,会抛出SyntaxError: "count" is read-only

上面的例子展示了ES6 Module中导入的变量其实是对原有值的动态映射。index.js中的count是对calculator.js中的count值的实时反映,当我们通过调用add函数更改了calculator.js中count值时,index.js中count的值也随之变化。

我们不可以对ES6 Module导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像


循环依赖

循环依赖循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A

// a.js
import { foo } from './b.js';
foo();

// b.js
import { bar } from './a.js';
bar();

一般来说工程中应该尽量避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,而循环依赖则会带来一定的复杂度。而在实际开发中,循环依赖有时会在我们不经意间产生,因为当工程的复杂度上升到足够规模时,就容易出现隐藏的循环依赖关系。

简单来说,A和B两个模块之间是否存在直接的循环依赖关系是很容易被发现的。但实际情况往往是A依赖于B,B依赖于C,C依赖于D,最后绕了一大圈,D又依赖于A。当中间模块太多时就很难发现A和B之间存在着隐式的循环依赖


CommonJS中循环依赖的例子

// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is `foo.js`';

// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';

// index.js
require('./foo.js');

在这里,index.js是执行入口,它加载了foo.js,foo.js和bar.js之间存在循环依赖。让我们观察foo.js和bar.js中的代码,理想状态下我们希望二者都能导入正确的值,并在控制台上输出。

value of foo: This is foo.js
value of bar: This is bar.js

而当运行上面的代码时,实际输出却是:

value of foo: {}
value of bar: This is bar.js

为什么foo的值会是一个空对象呢?
(1)index.js导入了foo.js,此时开始执行foo.js中的代码
(2)foo.js的第1句导入了bar.js,这时foo.js不会继续向下执行,而是进入了bar.js内部
(3)在bar.js中又对foo.js进行了require,这里产生了循环依赖。需要注意的是,执行权并不会再交回foo.js,而是直接取其导出值,也就是module.exports。但由于foo.js未执行完毕,导出值在这时为默认的空对象,因此当bar.js执行到打印语句时,控制台中的value of foo就是一个空对象。
(4)bar.js执行完毕,将执行权交回foo.js。
(5)foo.js从require语句继续向下执行,在控制台打印出valueof bar(这个值是正确的),整个流程结束。由上面可以看出,尽管循环依赖的模块均被执行了,但模块导入的值并不是我们想要的。因此在CommonJS中,若遇到循环依赖我们没有办法得到预想中的结果


从Webpack的实现角度来看,将上面例子打包后,bundle中有这样一段代码非常重要:

/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/ 	};

过程梳理一下

// The require function
function __webpack_require__(moduleId) {
  // 1.在index.js里require('./foo.js')的时候,跳过这个if
  // 3.在foo.js里require('./bar.js')的时候,跳过这个if
  // 5.在bar.js里require('./foo.js')的时候,执行了这个if逻辑,bar.js里的foo是{}
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // Create a new module (and put it into the cache)
  // 2.创建新模块缓存起来,记录foo里面的东西
  // 4.创建新模块缓存起来,记录bar里面的东西
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
  };
  ...

当index.js引用了foo.js之后,相当于执行了这个__webpack_require__函数,初始化了一个module对象并放入installedModules中。当bar.js再次引用foo.js时,又执行了该函数,但这次是直接从installedModules里面取值,此时它的module.exports是一个空对象。这就解释了上面在第3步看到的现象


使用ES6 Module的方式重写上面的例子

// foo.js
import bar from './bar.js';
console.log('value of bar:', bar);
export default 'This is foo.js';

// bar.js
import foo from './foo.js';
console.log('value of foo:', foo);
export default 'This is bar.js';

// index.js
import foo from './foo.js';

执行结果如下:

value of foo: undefined
value of bar: This is bar.js

很遗憾,在bar.js中同样无法得到foo.js正确的导出值,只不过和CommonJS默认导出一个空对象不同,这里获取到的是undefined(可以写一个html引入打包出来的js查看console的打印结果)

上面我们谈到,在导入一个模块时,CommonJS获取到的是值的拷贝,ES6 Module则是动态映射,那么我们能否利用ES6Module的特性使其支持循环依赖呢?请看下面这个例子:

//index.js
import foo from './foo.js';
foo('index.js');

// foo.js
import bar from './bar.js';
function foo(invoker) {
    console.log(invoker + ' invokes foo.js');
    bar('foo.js');
}
export default foo;

// bar.js
import foo from './foo.js';
let invoked = false;
function bar(invoker) {
    if(!invoked) {
        invoked = true;
        console.log(invoker + ' invokes bar.js');
        foo('bar.js');
    }
}
export default bar;

上面代码的执行结果如下:

index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js

可以看到,foo.js和bar.js这一对循环依赖的模块均获取到了正确的导出值。下面让我们分析一下代码的执行过程。
(1)index.js作为入口导入了foo.js,此时开始执行foo.js中的代码。
(2)从foo.js导入了bar.js,执行权交给bar.js。
(3)在bar.js中一直执行到其结束,完成bar函数的定义。注意,此时由于foo.js还没执行完,foo的值现在仍然是undefined。
(4)执行权回到foo.js继续执行直到其结束,完成foo函数的定义。由于ES6 Module动态映射的特性,此时在bar.js中foo的值已经从undefined成为了我们定义的函数,这是与CommonJS在解决循环依赖时的本质区别,CommonJS中导入的是值的拷贝,不会随着被夹在模块中原有值的变化而变化。
(5)执行权回到index.js并调用foo函数,此时会依次执行foo→bar→foo,并在控制台打出正确的值。

由上面的例子可以看出,ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值


总结:

CommonJS和ES6 Module是目前使用较为广泛的模块标准。它们的主要区别在于
1.前者建立模块依赖关系是在运行时,后者是在编译时;
2.在模块导入方面,CommonJS导入的是值拷贝,ES6 Module导入的是只读的变量映射;
3.ES6 Module通过其静态特性可以进行编译过程中的优化,并且具备处理循环依赖的能力

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未禾

您的支持是我最宝贵的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值