对require包加载源码的理解

前言

为了了解require导包的记载逻辑,在webstorm上打断点调试了无数次,让我对require导包逻辑和调试源码又有了新的理解

1.我对源码解读的流程

比较主观,不一定适合你,可参考

  1. 把玩儿模块的api,了解大致的使用规则
  2. 找一篇关于源码解读的文章,不一定全部都看懂记得里面出现频率比较高的函数或者字段等还有主干逻辑的位置
  3. 在webstorm打断点调试,遇到关键字就进入查看内部逻辑
  4. 兼容和参数校验或者缓存之类的分支逻辑跳过,只看主干逻辑
  5. 整理主干逻辑做了什么事情
  6. 对主干有一定了解以后,在回头看一些重要的,感兴趣的分支逻辑
  7. 对整个流程有一定了解以后,尝试画下流程图
  8. 一定要调试几遍,不要想的一次就搞定
  9. 尝试自己写实现下主干逻辑,或者某个微小的功能

2. 打断点,对主干逻辑的梳理

1.进入makeRequireFunction 调用mod.require(path) \
2.Module.prototype.require  参数校验, 加载模块(requireDepth记录加载深入) \
3.Module._load(id, this, /* isMain */ false);   加载模块; 前面做的缓存, \
  3.1  缓存模块,有缓存直接返回 \
  3.2 const filename = Module._resolveFilename(request, parent, isMain); 获取文件的绝对路径 \
  3.3 判断是否是内部模块,是就直接返回 \
  3.4 生成new module 对象(module) \
  3.5 缓存模块 module \
  3.4 return module.exports; \
4.module.load  获取文件后缀名  \
5.Module._compile   根据后缀名.js.json之类的,对应不同解析方式 \
  5.1.Module._extensions 读取js文件,通过 fs.readFileSync(filename, 'utf8'),最后返回module._compile \
  5.2 .module._compile 使用 有wrapSafe 给文件包裹一个函数,并执行,给module.exports赋值 \
6.  最终require方法会拿到 module.exports 返回结果
复制代码

3. 我对require加载逻辑的理解

画了一张主干逻辑的流程图:

image.png

读取包的时候都用了warpSafe函数在外面包裹了下,module,exports,required,__dirname,__filename
这几个参数都是在文件中可以直接调用的,但并不是node全局属性. 看下图:

// warpSafe函数
function fn(module,exports,required,__dirname,__filename){
	module.exports = hello
       return module.exports 

}
复制代码

4.实现一个require

只实现了主干逻辑,可直接执行
没有对参数校验,路径拼接
对Module的对象也做了属性简化,其实有很多属性

//自己实现一个包加载的逻辑
 
const path = require("path")
const fs = require("fs")
const vm = require("vm")

// 返回绝对路径
Module._resolveFilename = function (filename) {
    const filePath = path.resolve(__dirname, filename)
    let exists = fs.existsSync(filePath)
    if (exists) return filePath   //如果存在就直接返回
    // 文件不存在就尝试添加, .js和 json 后缀
    let keys = Reflect.ownKeys(Module._extensions)  // 返回Module._extensions的所有方法名
    for (let i = 0; i < keys.length; i++) {
        let newPath = filePath + keys[i]
        if (fs.existsSync(newPath)) return newPath  // 加完后缀,如果存在就返回
    }
    throw  new Error("module not found")
}

// 更具不同的文件加载不同的策略
Module._extensions = {
    '.js'(module) {
        // 读取文件
        let script = fs.readFileSync(module.id, 'utf8')
        // 用函数包裹,生成一个字符的模板
        let template = `(function(exports,module,require,__filename,__dirname){${script}})`;
        // 根据模板和当前js的上下文生成一个函数
        let compileFunction = vm.runInThisContext(template);
        let exports = module.exports; // 为了实现一个简写
        let thisValue = exports; //  this = exports = module.exports = {}
        let filename = module.id; // 去除文件路径
        let dirname = path.dirname(filename) // 去除当前文件夹
        // 执行模板函数,并且给module.export赋值
        compileFunction.call(thisValue, exports, module, myRequire, filename, dirname)

    },
    '.json'(module) {
        let script = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(script); // 直接将json挂载到exports 对象上,这样用户可以直接require一个json文件,拿到的就是json的内容
    }
}

// 加载模块
Module.prototype.load = function () {
    let extension = path.extname(this.id); //  取出文件扩展名(后缀名)
    // 根据后缀名来实现不同的加载逻辑
    Module._extensions[extension] && Module._extensions[extension](this) //Module存在该文件后缀名方法并且执行
}


function Module(id) {
    this.id = id   // 绝对路径
    this.exports = {} //模块对应的导出结果
}

Module._cache = {}  //用来缓存


function myRequire(filename) {
    let filePath = Module._resolveFilename(filename)
    let exists = Module._cache[filePath]
    if (exists) {
        return exists.exports
    }
    //2.创建一个模块
    let module = new Module(filePath)
    // 3. 缓存模块
    Module._cache[filePath] = module
    // 获取模块中的内容,包装函数,让函数执行.用户的逻辑会给module,export 赋值
    module.load()
    console.log(module)
    return module.exports // 最后的结果
}
const r = myRequire('./main.js');
console.log(r,123);

复制代码

最后

本文只是我对require的理解,并不能保证权威严禁,不过可以帮助你对require导包逻辑有一些帮助,希望你多打断点调试下~

如果对你有用,就点个赞吧~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享