动态引入之require的实现原理

476 阅读3分钟

关键词:

  • 社区标准
  • 使用函数实现
  • 仅node环境支持
  • 动态依赖(需要代码运行后才能确定依赖)
  • 动态依赖是同步执行的

再实现require引入之前我们先了解一个问题

问题:字符串如何能变成js执行

1.eval()函数会将传入的字符串当做 JavaScript 代码进行执行

console.log(eval('2 + 2'));
// Expected output: 4

console.log(eval(new String('2 + 2')));
// Expected output: 2 + 2

console.log(eval('2 + 2') === eval('4'));
// Expected output: true

2.new Fuction ‘模板引擎的实现原理’ 可以获取全局变量,还是会有污染的信息

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// Expected output: 8

3.node中自己实现了一个模块vm,不受环境(沙箱环境)

var a = 1
const vm = require('vm');
// 在node中全局变量是在多个模块下共享的, 所以不要通过global来定义属性
let fn = vm.runInThisContext(`(function(a){console.log(a)})`); 
fn(a)
// Expected output: 1

require的实现

  1. require方法 -> Module.protoype.require方法
  2. Module._load 加载模块
  3. Module._resolveFilename 方法就是把路径变成了绝对路径 添加后缀名 (.js .json) .node
  4. new Module 拿到绝对路径创造一个模块 this.id exports = {}
  5. module.load 对模块进行加载
  6. 根据文件后缀 Module._extensions['.js'] 去做策略加载
  7. 用的是同步读取文件
  8. 增加一个函数的壳子 并且让函数执行 让 module.exports 作为了this
  9. 用户会默认拿到module.exports的返回结果

最终返回的是 exports对象

1.1 先写一个req函数

const fs = require('fs');
const path = require('path');
const vm = require('vm');

function req(filename){
    filename = Module._resolveFilename(filename); // 1.创造一个绝对引用地址,方便后续读取
    const module = new Module(filename); // 2.根据路径创造一个模块
    module.load(); // 就是让用户给module.exports 赋值
    return module.exports; // 默认是空对象
}

1.2 创建Module函数

function Module(id){
    this.id = id;
    this.exports = {}
}

1.3 _resolveFilename函数,获取文件的绝对路径

Module._resolveFilename = function (id) {
    let filePath = path.resolve(__dirname,id)
    let isExists = fs.existsSync(filePath);
    if(isExists) return filePath;
    // 尝试添加后缀
    let keys = Object.keys(Module._extensions); // 以后Object的新出的方法 都会放到Reflect上
    
    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')
}

1.4 加载load函数,就是让用户给module.exports赋值

Module.prototype.load = function (){
    let ext = path.extname(this.id); // 获取文件后缀名
    Module._extensions[ext](this);
}

1.5 根据文件的后缀名,去做策略加载,利用node中的vm模块,加载对应的模块,给module.exports赋值

Module._extensions = {
    '.js'(module){
        let script = fs.readFileSync(module.id,'utf8');
        let templateFn = `(function(exports,module,require,__dirname,__filename){${script}})`;
        let fn = vm.runInThisContext(templateFn);
        let exports = module.exports;
        let thisValue = exports; // this = module.exports = exports;
        let filename = module.id;
        let dirname = path.dirname(filename);

        // 函数的call 的作用 1.改变this指向 2.让函数指向
        fn.call(thisValue,exports,module,req,dirname,filename); // 调用了a模块 module.exports = 100;
    },
    '.json'(module){
        let script = fs.readFileSync(module.id,'utf8');
        module.exports = JSON.parse(script)
    }
}

1.6 再多次引入文件的时候,缓存文件,防止多次访问,改造req函数

Module._cache = {}
function req(filename){
    filename = Module._resolveFilename(filename); // 1.创造一个绝对引用地址,方便后续读取
    let cacheModule = Module._cache[filename]
    if(cacheModule) return cacheModule.exports; // 直接将上次缓存的模块丢给你就ok了

    const module = new Module(filename); // 2.根据路径创造一个模块
    Module._cache[filename] = module; // 最终:缓存模块 根据的是文件名来缓存
    module.load(); // 就是让用户给module.exports 赋值
    return module.exports; // 默认是空对象
}