前端模块化的那些事

CommonJS

  • 最开始比较成熟的模块化思想是来自CommonJs
  • 其主要应用于Node.js
  • 每个文件就是一个Module实例
  • 通过require引入,通过moudule暴露内容
  • 每个模块被加载一次后会被缓存,所以文件中除了暴露内容外,一些同步逻辑(如console.log)只会触发一次
  • 文件的加载是同步进行的
  • 沙箱编译,require的js代码会被改造成立即执行函数,然后将运行得出的module.exports对象缓存,供下次使用,这也是为什么js文件只会被执行一次
  • 导出的是值得拷贝,区别于ES6模块,后面会提及

AMD

  • 客户端看到服务端有这个模块化的机制,也想引进,但是由于CommonJs是同步加载的,在服务器,由于文件都在文件系统中,读取加载很快,但客户端里,资源需要通过网络请求,依旧采用同步的方法,会导致性能骤降
  • 出于上述考虑,在客户端上提出了异步加载模块的规范,即AMD(Asynchronous Module Definition)
  • 模块的加载不会影响其后续代码的运行
  • 所有依赖该模块的代码,会被放到一个回调函数中,等加载完毕后处理
  • 通过require引入模块,除了模块外,还需要传入一个回调函数,在模块加载完成时调用
  • 通过define定义模块,加载完成后会执行factory
define(id?, dependencies?, factory);

RequireJS

  • RequireJS是基于AMD规范的一种实现
  • 原理是通过动态创建script标签,插入dom中,并在onload事件中执行回调
  • 加载过的模块会对其缓存

CMD

  • 类似于AMD,阿里自己搞了个CMD的规范,即(Common Module Definition)
  • 其通过define定义模块,require引入,通过exports或return暴露变量

SeaJS

  • SeaJS是CMD规范的一种实现
  • 与RequireJS不同,seaJS采用的是就近执行,RequireJS是前置执行
  • 模块的加载都是异步的,但执行上,RequireJS会默认先把所有的依赖都执行了,而SeaJS会按照你在代码中定义的顺序来执行
  • 加载过的模块会对其缓存

ES6

  • ES6问世后,模块化的概念被写进了ES里,提出了官方的模块化规范
  • 通过import引入,export导出,这里的export和import是关键字,之前的define和require等都是第三方包定义的全局方法
  • 与CommonJS不同的是,ES6输出的是值引用,CommonJS输出的是值拷贝
// test.js
const test = { a: 1 };
export.test = test;
export.add = () => test.a = test.a + 1;

// index.js
// in commonjs
const { test, add } = require(./test);
// or in es6
// import { test, add } from './test';
add();
console.log(test.a)
  • 上述例子,ES6输出的是2,CommonJS输出的是2,CommonJS输出是1
  • CommonJS的导出原理可以参考webpack针对CommonJS模块的打包处理,其原理是通过执行脚本,将导出的对象保存在内部的一个模块集合中,下次再读取模块就直接从该模块中取值,这里保存导出对象就是一个直接赋值的过程,所以CommonJS的导出是一个浅拷贝的过程,另外外部模块通过require引入的变量,其实是通过解构赋值,或直接赋值的形式引入的,且这些引入的变量可以修改其值,所以如果引入的是基本类型,后续修改他不会对输出造成影响,但如果引入的是对象,修改属性,会对导出模块中对应的属性造成影响
  • ES6中,JS引擎在静态分析阶段遇到import命令时,会生成一个只读引用,后续逻辑都是基于这个引用做的处理,当真正执行脚本时,才根据这个引用取对应模块里取值,所以他也不会缓存export,整个模块在运行环境里可以认为是一个对象,导出的变量都是绑定在这个模块上的,并且通过import引入的变量是不能修改其值的,要修改只能使用模块里本身的方法,这就保证了引入的变量永远是跟随模块中的变量发生变化的
  • 同理,基于ES6的运行原理,它属于编译时加载(静态加载),在编译阶段就可以知道引入的是哪个变量,对应输出他的引用,但CommonJS需要执行了整个模块,对应生成一个输出的对象,根据这个对象才能决定引入的值,这种模式称为运行时加载
  • 运行时加载由于需要运行整个代码块,所以无法做静态优化,编译时加载由于在编译阶段就知道需要的变量,所以可以在编译阶段动态删除无用的代码,减少打包的体积
  • 注意几个例子
// test.js
let count = 1;
export default {
    count,
    add() { count++; },
    get() { return count };
}

// index.js
import test from './test.js'
console.log(test.count); // 1
test.add();
console.log(test.count); // 1
console.log(test.get()); // 2
  • 这里test.add并没有改变test.count的值,是因为count是基本类型,导出到export的count中属于值拷贝,后续操作不会改变他的值,想要改变count的值能同步更新,应该把他输出到导出中,即
// test.js
export let count = 1;
export function add() { count++; }
export function get() { return count }

// index.js
import {count, add, get} from './test.js'
console.log(count); // 1
add();
console.log(count); // 2
console.log(get()); // 2
  • count现在作为输出,即使他是一个基本类型,外部模块改变了它,输出也会同步更新,是因为外部引入的count已经是一个引用,指向的是模块中的数值

你可能感兴趣的:(前端)