文章目录
1. 模块化是什么?
模块化是一种处理复杂系统分解成为更好的可管理模块的方式,它可以把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化如何影响其它部分就会变得显而易见,使得系统更容易维护。
2. 模块化的好处
模块化的优势有以下几点:
(1) 防止命名冲突
(2) 代码可复用
(3) 高可维护性
3. 模块化规范
-
在早期,使用闭包实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
-
现代模块化机制
(1)CommonJS
(2)AMD
(3)CMD
(4)ESM(es6模块化)以上四种现代模块化机制的模块加载是同步的还是异步的
- CommonJS是同步的
commonjs设计出来就是为了node使用的,对node来说,模块存放在本地硬盘,同步加载,等待时间就是硬盘的读取时间,这个时间非常短; - AMD、CMD是异步的
模块存放在服务器上,需要通过网络请求,浏览器加载模块的时间取决于网络的好坏,可能要等很长时间,因此设计为异步的 - ESM可以看作是异步的
设计es6模块的时候,并没有强行指定同步或异步。但是所有浏览器对<script type="module"></script>
都会默认加defer,所以可以认为是异步的。
- CommonJS是同步的
1. CommonJS【同步】
NodeJS、webpack 是以CommonJS规范的实现
-
基础语法:
- 暴露模块:
module.exports = ???
exports.XXX = ???
实质上暴露的是exports对象module.exports = exports = {} - 引入模块:
引入第三方模块:require(模块名)
引入自定义模块:require(模块文件路径)
- 暴露模块:
-
实现
- 服务器端:node
- 浏览器端:Browserify(也称为js的打包工具)
1.服务器端模块化----node
- 下载安装node.js
- 创建项目结构
|-modules |-module1.js |-module2.js |-module3.js |-app.js |-package.json { "name": "commonJS-node", "version": "1.0.0" }
- 下载第三方模块
- npm install uniq --save
- 模块化编码
-
module1.js
module.exports = { foo() { console.log('moudle1 foo()') } }
-
module2.js
module.exports = function () { console.log('module2()') }
-
module3.js
exports.foo = function () { console.log('module3 foo()') } exports.bar = function () { console.log('module3 bar()') }
-
app.js
/** 1. 定义暴露模块: module.exports = value; exports.xxx = value; 2. 引入模块: var module = require(模块名或模块路径); */ "use strict"; //引用模块 let module1 = require('./modules/module1') let module2 = require('./modules/module2') let module3 = require('./modules/module3') //使用模块 module1.foo() module2() module3.foo() module3.bar()
- 通过node运行app.js
- 命令: node app.js
- 工具: 右键–>运行
2.浏览器端模块化----Browserify
-
创建项目结构
|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件 |-index.html |-package.json { "name": "browserify-test", "version": "1.0.0" }
-
下载browserify
- 全局: npm install browserify -g
- 局部: npm install browserify --save
-
定义模块代码
-
module1.js
module.exports = { foo() { console.log('moudle1 foo()') } }
-
module2.js
module.exports = function () { console.log('module2()') }
-
module3.js
exports.foo = function () { console.log('module3 foo()') } exports.bar = function () { console.log('module3 bar()') }
-
app.js (应用的主js)
//引用模块 let module1 = require('./module1') let module2 = require('./module2') let module3 = require('./module3') //使用模块 module1.foo() module2() module3.foo() module3.bar()
- 打包处理js:
browserify js/src/app.js(浏览器引入的js文件) -o js/dist/bundle.js(打包生成的js文件)
- 页面使用引入:
<script type="text/javascript" src="js/dist/bundle.js"></script>
2. AMD【异步】
专门位于浏览器端,模块加载是异步的,使用require.js
-
基础语法:
- 暴露模块: define([依赖模块名], function(){return 暴露的模块对象})
- 引入模块: require([‘模块1’, ‘模块2’, ‘模块3’], function(m1, m2){//使用模块对象})
require.js使用
- 下载require.js, 并引入
- 官网: http://www.requirejs.cn/
- 将require.js导入项目: js/libs/require.js
- 创建项目结构
|-js |-libs |-require.js |-modules |-alerter.js |-dataService.js |-main.js |-index.html
- 定义require.js的模块代码
- dataService.js
define(function () { let msg = 'atguigu.com' function getMsg() { return msg.toUpperCase() } //向外暴露 return {getMsg} })
- alerter.js
//依赖dataService.js //'dataService'与paths里的key对映 //第三方jQuery必须是'jquery',不需要路径映射,在jQuery源码中封装过了。 define(['dataService', 'jquery'], function (dataService, $) { let name = 'Tom2' function showMsg() { $('body').css('background', 'gray') alert(dataService.getMsg() + ', ' + name) } //向外暴漏 return {showMsg} })
-
应用主(入口)js: main.js
(function () { //配置 requirejs.config({ //基本路径 baseUrl: "js/", //模块标识名与模块路径映射 paths: { "alerter": "modules/alerter", "dataService": "modules/dataService", } }) //引入使用模块 //'alerter'与paths里的key对映 requirejs( ['alerter'], function(alerter) { alerter.showMsg() }) })()
-
页面使用模块:
<script data-main="js/main" src="js/libs/require.js"></script>
3. CMD【异步】
略。。。
AMD和CMD的区别
对依赖模块的执行时机处理不同(注意不是加载的时机)
AMD依赖前置、提前执行,在定义模块的时候就声明其依赖的模块,方便知道依赖了哪些模块,然后马上加载 , 在加载完成后, 就会执行该模块;
CMD依赖就近、延迟执行,把模块变为字符串解析一遍, 找到依赖了哪些模块, 在加载模块完成后, 不立刻执行, 而是等到require后再执行
4. es6模块化【异步】
位于浏览器端的模块化
-
基础语法:
export命令用于规定暴露模块
import命令用于引入模块
< script type = “module”>
//所有暴漏的数据都存放于m中
import * as m from “xxx/xxx/xxx.js”
< /script > -
创建目录结构
|-js |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件 |-index.html
//module1.js //分别暴露 export let name = "张颜齐" export function lable(){ console.log("远赴人间惊鸿宴,一睹世间盛世颜"); } //module2.js //统一暴露 let name = "张颜齐" function lable(){ console.log("远赴人间惊鸿宴,一睹世间盛世颜"); } export { name,lable } //module3.js //默认暴露 export default{ name: "张颜齐", lable(){ console.log("远赴人间惊鸿宴,一睹世间盛世颜"); } } //app.js //入口js //1.通用形式导入 import * as m1 from "./module1.js"; console.log(m1); m1.lable(); import * as m2 from "./module2.js"; console.log(m2); m2.lable(); import * as m3 from "./module3.js"; console.log(m3); m3.default.lable(); //2. 解构复制形式 //2.1 分别暴露和统一暴露 import {name,lable} from "./module1.js"; console.log(lable); //2.2 默认暴露,必须使用别名,不能使用default import {default as m4} from "./module3.js"; console.log(m4); //3.简便形式,只针对默认暴漏 import m5 from "./module3.js"; console.log(m5); //app.html <script src = "./js/app.js" type = "module"/></script>
- CommonJs是使用module.exports或exports导出,使用require导入;ESModule则是使用export导出,使用import导入。
- Commonjs有缓存:require值会缓存,通过require引用文件时,会将文件执行一遍后,把结果进行缓存,后续require该路径,直接从内存获取,无需重新执行文件
- Commonjs 模块导出是值的拷贝,而 ES module 是引用拷贝。Commonjs模块输出是值的拷贝,一但输出,模块内部变化后,无法影响之前的引用,而ESModule是引用拷贝。ESM会在底层维护一个modelRecord树结构,类似于一个模块的操作记录,会根据你的import入口,将全部依赖梳理出来,modelRecord里面存的全部是引用地址
- CommonJs是运行时加载模块,ESModule是在静态编译期间确定模块的依赖。
- CommonJs不会提升require,ESModule在编译期间会将所有import提升到顶部
按需加载的本质就是利用了promise
面试题:
- 谈谈你对闭包的理解
- commonJS 和 ESM 分别如何解决循环依赖的?
假设A引用B,B又引用A- commonJS :
引用A模块的时候,首先执行A引用B之前的代码(A1),会在缓存中先缓存一份未完成的副本A1,遇到A引用B的时候,先执行B引用A之前的代码(B1),然后在B引用A时,从缓存中拿到已缓存的副本A,不会来回循环调用,然后接着走完B引用A后面的代码(B2),当完全执行完B模块之后,再走回A模块,去执行a引用b后面的代码(A2)
- ESM:
import命令具有提升效果,会提升到整个模块的头部,首先执行。
处理顺序为:预处理 B -> 预处理 A -> 执行 B -> 执行 A。
esm会给每个模块打一个tag,标识这个模块的状态
- commonJS :