背景
网景公司的那个胖子,用了七天的时间开发出了 JavaScript,JS最初的目标是实现简单的页面交互:页面动画 + 提交表单。此时并无模块化或命名空间的基础。
但计算机硬件的发展,用户的电脑越来越好,业务人员希望网页能够实现越来越复杂的功能,开发人员也愿意为此付出劳动,随着业务逻辑日趋复杂,简单的JS无法再满足人们的需求,于是模块化需求日益增长。
我们可以把模块化的发展氛围四个时期:幼年期、成长期、成熟期和完全体。那么废话不多说,开始吧!
幼年期:以js文件为维度的委婉的模块化
开始出现各种js库:动画、表单、格式化等。以js文件的形式被加载。你最初学习 html 时,应该写过如下代码,这是文件分离最基础的模块化方式。
<!-- 库文件 -->
<script src="tools.js"></script>
<script src="map.js"></script>
<!-- 主文件 -->
<script src="main.js"></script>
这里会涉及到 script 两个参数 :async 和 defer
<script src="tools.js" async></script>
<script src="map.js" defer></script>
浏览器加载一个js文件时,先下载,再解析,那上面两个参数区别是?
普通(不加参数):同步下载,下载文件,解析文件,执行后续代码。
defer:异步下载,下载文件,执行后续代码,待DOM解析完成,再解析文件。
async: 异步下载,下载文件,执行后续代码,下载完成后解析文件,阻塞线程。
这个问题常常伴随着话题导向:浏览器渲染原理、同步异步原理、模块化加载原理。本文介绍的是第三个。
这种模块化的缺点
污染全局作用域,不利于大型项目或涉及多人团队共建。
成长期:模块化初现 - IIFE
IIFE:立即执行函数
这个时期并没有出现新的技术,只是利用了语法的优势:函数的独立作用域进行的一种写法优化。
举个例子,我写一段幼年期的模块文件
// count.js
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 调用方式,全局调用
increase()
reset()
按照幼年期的引入方式,这个 count 就成了全局变量,会污染全局。
而我使用 IIFE,一个立即执行的函数,其写法如下
const module = (() => {
let count = 0;
return {
increase: () => ++count,
reset: () => {
count = 0;
}
}
})();
// 使用方式,挂在在 module 下调用
module.increase();
module.reset();
这样 count 就成了一个局部变量,要使用它时,就需要通过 module 模块,这就是 IIFE 的魅力 ,它可以抽象成以下方式,即定义了一个匿名函数并直接执行。
(() => {
let count = 0;
// ...
})()
这是最最最简单的模块化,也是最最最基础的模块化,你可以在 jquery 中看到它的身影,你甚至在 webpack 中配置打包方式设置为 "IIFE" 时,你打包出来的代码也是这样的格式,它简单但也很流行。
问题:如果我的库依赖外部库,怎么办呢?答案是在 IIFE 里传参即可,放码过来
const module = ((dependencyModule1, dependencyModule2) => {
//
})(dependencyModule1, dependencyModule2);
成熟期:百花齐放
CommonJS
CommonJS极大地推动了 JavaScript 的发展,它直接改变了生态(破圈了)!因为它不仅仅实现了前端模块化,它还直接可以让 JS 向服务端发展,最典型的就是 NodeJs,NodeJs的制定就是以它为标准的。它的主要特征是
- 通过 module + export 向外暴露接口
- 通过 require 调用其他接口
show一段代码看看 CJS 是如何使用
// 在 count.js 中
// 引入部分
const dependencyModule1 = require(../dependencyModule1)
const dependencyModule2 = require(../dependencyModule2)
let count = 0
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 暴露的接口
module.exports = {
increase,
reset
}
优点
- 解决了全局污染的问题。
- 完完全全从主观感受上实现了模块化,虽然 IIFE 也是模块化,但写法挺没有”模块感“。
缺点:require 是一个同步加载的过程,不能解决异步问题。
AMD:异步加载 + 回调函数
最经典的框架是 require.js,其大致代码可表示为如下
define(
"amdModule",
["dependencyModule1", "dependencyModule2"],
(dependencyModule1, dependencyModule2) => {
})
require(["amdModule"], amdModule => {
amdModule.increse()
})
如果一个模块,既使用了 CJS ,又使用了 AMD,那么应该如何区分呢?UMD。
AMD的优点解决了服务端,客户端的动态以来问题,也实现了异步加载。
缺点是,没有按需加载。需要先加载所有的依赖,才能触发回调,但在回调里的代码可能并不会全部执行,可能在某一个分支就 return 了,这样的话后续代码就用不到某些依赖。
CMD:按需加载,依赖就近
主要框架:sea.js
define('module', (require, exports, module) => {
let $ = require('map');
if (xxx) {
return;
}
let depends2 = require('./dependencyModule2');
})
缺点是
- 依赖打包,需要编译(虽然现在工程化都需要打包,但其实还有更好的模块化方法,正因为有更好的方法,所以以来打包就变成了不足之处,继续读下文)
- 扩大了模块化内的体积
完全体:ESM,走进新时代
ES6新增标准:引入 import,导出 export。我们不再需要第三方库,就能实现模块化,ES6已经把模块化这个思想写进 JavaScript 规则里,只要调用 import 和 export 就能实现模块化,不管是异步加载,还是按需加载。
// 引入区域
import dependencyModule1 from './dependencyModule1';
// 逻辑处理
// ……
export default {
increase, reset
}
// 动态加载
import("./dependencyModule1").then(dependencyModule1 => {})
模块化给编程带来的改变
- 模块化:解耦、组件化。
- 面向过程转变成面向对象。
- 模块化的基础造就了函数式编程。
函数式编程
每个函数都是一个模块,将大问题转化成小问题,将复杂问题转化成简单问题,最典型的就是 React,React的每个组件都是一个 function 函数。
function Component() {
const [count, increaseCount] = useState(0)
const onClick = () => {
increaseCount(++count)
}
return (<>
<button onClick={onClick}>{count}</button>
</>)
}