注: 1. 本文涉及的 Node.JS 源码如无特别说明则全部基于 https://github.com/nodejs/node/tree/v10.14.1
如果你对 Node.JS 系列感兴趣, 欢迎关注前端神盾局或笔者微信 (w979436427) 交流讨论 node 学习心得
Node.JS 中对模块的实现
本节主要基于 Node.JS 源码, 对其模块的实现做一个简要的概述, 如有错漏, 望诸君不吝指正.
当我们使用 require 引入一个模块的时候, 概况起来经历了两个步骤: 路径分析和模块载入
路径分析
路径分析其实就是模块查找的过程, 由_resolveFilename 函数实现.
我们通过一个例子, 展开说明:
- const http = require('http');
- const moduleA = requie('./parent/moduleA');
这个例子中, 我们引入两种不同类型的模块: 核心模块 - http 和自定义模块 moduleA
对于核心模块而言,_resolveFilename 会跳过查找步骤, 直接返回, 交给下一步处理
- if (NativeModule.nonInternalExists(request)) {
- // 这里的 request 就是模块名称'http'
- return request;
- }
而对于自定义模块而言, 存在以下几种情况(_findPath)
文件模块
目录模块
从 node_modules 目录加载
全局目录加载
这些在官方文档 http://nodejs.cn/api/modules.html#modules_modules 中已经阐述的很清楚了, 这里就不再赘述.
如果模块存在, 那么_resolveFilename 会返回该模块的绝对路径, 比如 / Users/xxx/Desktop/practice/node/module/parent/moduleA.JS.
载入模块
获取到模块地址后, Node 就开始着手载入模块.
首先, Node 会查看模块是否存在缓存中:
- // filename 即模块绝对路径
- var cachedModule = Module._cache[filename];
- if (cachedModule) {
- updateChildren(parent, cachedModule, true);
- return cachedModule.exports;
- }
存在则返回对应缓存内容, 不存在则进一步判断该模块是否是核心模块:
- if (NativeModule.nonInternalExists(filename)) {
- return NativeModule.require(filename);
- }
如果模块既不存在于缓存中也非核心模块, 那么 Node 会实例化一个全新的模块对象
- function Module(id, parent){
- // 通常是模块绝对路径
- this.id = id;
- // 要导出的内容
- this.exports = {};
- // 父级模块
- this.parent = parent;
- this.filename = null;
- // 是否已经加载成功
- this.loaded = false;
- // 子模块
- this.children = [];
- }
- var module = new Module(filename, parent);
而后 Node 会根据路径尝试载入.
- function tryModuleLoad(module, filename) {
- var threw = true;
- try {
- module.load(filename);
- threw = false;
- } finally {
- if (threw) {
- delete Module._cache[filename];
- }
- }
- }
对于不同的文件扩展名, 其载入方法也有所不同.
.JS 文件(_compile)
通过 fs 同步读取文件内容后将其包裹在指定函数中:
- Module.wrapper = [
- '(function (exports, require, module, __filename, __dirname) {',
- '\n});'
- ];
调用执行此函数:
- compiledWrapper.call(this.exports, this.exports, require, this,
- filename, dirname);
.JSON 文件
通过 fs 同步读取文件内容后, 用 JSON.parse 解析并返回内容
- var content = fs.readFileSync(filename, 'utf8');
- try {
- module.exports = JSON.parse(stripBOM(content));
- } catch (err) {
- err.message = filename + ':' + err.message;
- throw err;
- }
.node
这是用 C/C++ 编写的扩展文件, 通过 dlopen()方法加载最后编译生成的文件.
return process.dlopen(module, path.toNamespacedPath(filename));
.mjs
这是用于处理 ES6 模块的扩展文件, 是 Node.JS 在 v8.5.0 后新增的特性. 对于这类扩展名的文件, 只能使用 ES6 模块语法 import 引入, 否则将会报错(启用 --experimental-modules 的情况下)
throw new ERR_REQUIRE_ESM(filename);
如果一切顺利, 就会返回附加在 exports 对象上的内容
return module.exports;
模块循环依赖
接下来我们来探究一下模块循环依赖的问题: 模块 1 依赖模块 2, 模块 2 依赖模块 1, 会发生什么?
这里只探究 commonjs 的情况
为此, 我们创建了两个文件, module-a.JS 和 module-b.JS, 并让他们相互引用:
- module-a.JS
- console.log('开始加载 A 模块');
- exports.a = 2;
- require('./module-b.js');
- exports.b = 3;
- console.log('A 模块加载完毕');
- module-b.JS
- console.log('开始加载 B 模块');
- let moduleA = require('./module-a.js');
- console.log(moduleA.a,moduleA.b)
- console.log('B 模块加载完毕');
运行 module-a.JS, 可以看到控制台输出:
开始加载 A 模块
开始加载 B 模块
2 undefined
B 模块加载完毕
A 模块加载完毕
这时因为每个 require 都是同步执行的, 在 module-a 完全加载前需要先加载./module-b, 此时对于 module-a 而言, 其 exports 对象上只附加了属性 a, 属性 b 是在./module-b 加载完成后才赋值的.
QA
如何删除模块缓存?
可以通过 delete require.cache(moduleId)来删除对应模块的缓存, 其中 moduleId 表示的是模块的绝对路径, 一般的, 如果我们需要对某些模块进行热更新, 可以使用此特性, 举个例子:
- // hot-reload.JS
- console.log('this is hot reload module');
- // index.JS
- const path = require('path');
- const fs = require('fs');
- const hotReloadId = path.join(__dirname,'./hot-reload.js');
- const watcher = fs.watch(hotReloadId);
- watcher.on('change',(eventType,filename)=>{
- if(eventType === 'change'){
- delete require.cache[hotReloadId];
- require(hotReloadId);
- }
- });
Node 中可以使用 ES6 模块吗?
从 8.5.0 版本开始, Node.JS 开始支持原生 ES6 模块, 启用该功能需要两个条件:
所有使用 ES6 模块的文件扩展名都必须是. mjs
命令行选项 --experimental-modules node --experimental-modules index.mjs
node --experimental-modules index.mjs
但是截止到 Node.JS v10.15.0,ES6 模块的支持依旧是实验性的, 笔者并不推荐在公司项目中使用
参考
Node.JS-loader.JS
朴灵. 深入浅出 Node.JS
来源: https://juejin.im/post/5c43352ff265da6142742bf7