JS模块模式 | 青训营笔记

105 阅读6分钟

前言

这是我参与「第四届青训营 」笔记创作活动的第 5 天,今天给大家分享一下我学习JavaScript模块模式的笔记。

模块模式

逻辑分块,各自封装,相互独立,每个块自行决定对外暴露的东西和引入执行哪些外部代码。

模块标识符

每个模块都有一个用于引用它的标识符,此标识符在模拟模块的系统中可能是字符串,在原生实现的模块系统中可能是模块文件的实际路径。

模块依赖

模块系统的核心是管理依赖。

本地模块向模块系统声明一组外部模块,也称为依赖,这些依赖对于当前模块的正常运行是必需的。

模块系统检视这些依赖来保证本地模块运行时能够加载并初始化所有外部依赖。

每个模块都会与某个唯一的标识符关联,该标识符可用于检索模块。这个标识符通常是 JavaScript文件的路径,但在某些模块系统中,这个标识符也可以是在模块本身内部声明的命名空间路径字符串。

模块加载

本地模块在执行依赖时,必须保证依赖已经准备好并初始化。

  • 浏览器首先要请求依赖代码

  • 收到之后确认收到的模块是否也有依赖

    • 递归评估并加载所有依赖,直到所有依赖模块都加载完成
  • 加载完所有依赖后,才可以执行入口模块

一般的模块加载是阻塞的,如script标签引入

入口

相互依赖的模块必须指定一个模块作为入口,作为代码执行的起点。入口模块也有其他的依赖,其他模块也有。

异步依赖

按需加载模块的方法。

在模块A中请求加载模块B,而不是直接将所有可能的依赖一次性请求。

这样在页面加载时只需要同步加载一个文件。

动态依赖

可以在程序结构中动态添加依赖,例如:

if(loadCondition){
    require('./moduleA')
}

在运行时根据条件来确定是否需要加载依赖

静态分析

模块中包含的代码在浏览器中会被静态分析,主要包括检查代码结构和推断代码行为。动态依赖会使得静态分析较困难。

循环依赖

各个模块之间互相依赖,则在加载时会造成循环依赖,此时加载顺序会根据深度优先(加载浅的)来进行:

image.png

模块系统

ES6之前的原生模块系统

使用立即执行函数将模块封装在匿名闭包中,一般会返回一个对象来暴露公共API,例如:

const Foo = (function() { 
     return { 
         bar: 'baz', 
         baz: function() { 
         	console.log(this.bar); 
         } 
     }; 
})();

ES6之前的模块加载器

通常需要在浏览器中额外加载库或在构建时完成预处理,以把模块语法与JS连接起来。

CommonJS

是一个规范,概述了同步声明依赖的模块定义。

主要用于在服务器端实现模块化代码组织,也可以用于定义在浏览器中使用的模块依赖,该语法不能直接在浏览器中运行。

以服务器端为目标环境,能够一次性把所有模块都加载到内存。

Nodejs使用的是修改版的CommonJS。

在此规范中,模块不会指定自己的标识符,标识符由文件位置决定。

没有封装的CommonJS代码在浏览器中执行会创建全局变量,因此一般是提前把模块文件打包号,把全局属性转换为原生JS结构,将模块代码封装在函数闭包中,最终只提供一个文件。

语法

  • require(url):引入指定url的模块,同一个模块无论被require多少次都只会被加载一次

  • module.exports = {} :暴露自己的API

    exports是一个对象,也可以直接exports.key = xxx 来定义,可以有多个

    可以直接导出类或类实例:

    class A {} 
    module.exports = A; 
    const A = require('./moduleA'); 
    const a = new A(); 
    // 也可以将类实例作为导出值:
    class A {} 
    module.exports = new A();
    
  • 支持动态依赖:

    if (condition) { 
        const A = require('./moduleA'); 
    }
    

ES6模块

无需加载器和其他预处理

模块标签和定义

引入外部模块的方式:

// 外部文件加载
<script type="module" src="path/to/myModule.js"></script>
// 嵌入模块定义
<script type="module"> 
 // ...
</script>

type为module则告诉浏览器相关代码应该作为模块执行,而非传统的脚本执行。

模块标签执行的顺序与在页面中出现的顺序相同。

解析到模块标签会立即下载模块文件,但执行会延迟到文档解析完成,因此顺序只会英希昂文件的加载,而不会影响模块的加载。

同一个模块只会加载一次。

defer和async属性对加载的影响:

  • 如果 async="async":脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行)
  • 如果不使用 async 且 defer="defer":脚本将在页面完成解析时执行
  • 如果既不使用 async 也不使用 defer:在浏览器继续解析页面之前,立即读取并执行脚本

模块加载

ES6的模块既可以通过浏览器原生加载,也可以与第三方加载器或构建工具一起加载。

完全支持ES6模块的浏览器可以从顶级模块异步加载整个依赖图:

  • 解析入口模块,确定依赖并发送对依赖的请求

  • 收到依赖后解析内容,确定依赖模块各自的依赖(二级依赖),请求其中未加载的模块

    整个递归加载过程会持续到整个程序的依赖图解析完成

  • 依赖图接续完毕,应用程序正式加载模块

模块特性

  • 模块代码只能在加载后执行
  • 模块只能加载一次
  • 模块是单例
  • 模块可以定义公共接口,其他模块可以基于整个接口观察和交互
  • 模块可以请求加载其他模块
  • 支持循环依赖
  • 默认在严格模式下执行
  • 不共享全局命名空间
  • 顶级this是undefined,常规脚本是window
  • 模块中的var声明不会添加到window对象
  • ES6的模块是异步加载和执行的

模块导出

  • 命名导出

    export { something }

    export { something as name } 设置别名

  • 默认导出:export default

    行内默认导出不能出现变量声明,如const

    不能有别名

  • 转移导出

    export * from './foo.js'
    

模块导入

import,需要出现在模块的顶级,不能被任何块包起来。

import * as Foo from './foo.js';
指将foo中所有导出的东西导入,并用Foo保存

参考

《JavaScript高级程序设计》