require之模块加载流程

379 阅读2分钟

总述

本文仅以文件模块加载过程为例,梳理nodejs中模块加载的核心流程。步骤如下:

  1. 路径分析:获取当前文件绝对路径
  2. 文件定位:判定当前文件类型(js、json、node),以便后续调用相应编译函数
  3. 缓存优先:提高模块加载效率,加载前先查询缓存是否已加载过
  4. 编译执行:将模块内容编译成当前模块中直接使用的数据

测试文件

创建一个test.js测试文件,内容如下:

const str = 'test'
module.exports = str

再创建一个testJ.json测试文件,内容如下:

{
    "name": "lyk",
    "age": 29
}

代码结构

创建一个require.js文件,确定需要引入的内置模块,自定义myRequire方法,并加载测试文件,实现基本结构如下:

const fs = require('fs')
const path = require('path')
const vm = require('vm')    //将字符串处理成可执行代码,类似eval

function myRequire(filename){
    //...code
}

let str = myRequire('./test')
console.log('str', str)

将所有代码都置于...code处,会过于臃肿,此处模仿nodejs源码,定义一个Module类,在myRequire外部处理每一步骤的详细过程。在myRequire文件中定义Module,如下:

class Module{
    constructor(id){
        this.id = id       //id为模块标识
        this.exports = {}  //未来导出数据的容器
    }
}

步骤一:获取绝对路径

function myRequire(filename){
    // 1:获取绝对路径
    let absPath = Module._resolveFilename(filename)
}
Module._resolveFilename = function (filename){
    // 使用path将filename转为绝对路径
    let absPath = path.resolve(__dirname, filename)
    //判断当前路径对应内容是否存在
    if(fs.existsSync(absPath)){
        // 存在则直接返回
        return absPath
    }else{
        // 不存在则进行文件定位,对文件进行后缀补足,再查询文件是否存在
        let suffix = Object.keys(Module._extensions)
        console.log(suffix)  // ['.js', '.json']
        for (let index = 0; index < suffix.length; index++) {
            let path = absPath + suffix[index]
            if(fs.existsSync(path)){
                return path
            }
        }
    }
    throw new Error(`${filename} is not exists`)
}

对应后缀文件的处理函数(此处node后缀不作处理),后续步骤实现

Module._extensions = {
    '.js'(){},
    '.json'(){}
}

步骤二:查询缓存

function myRequire(filename){
    // ...
    // 2.缓存优先
    let cache = Module._cache[absPath]
    if(cache) return cache.exports
}
Module._cache = {}

步骤三:缓存未命中

function myRequire(filename){
    // ...
    // 3.创建空对象加载目标模块
    let module = new Module(absPath)
    // 写入缓存
    Module._cache[absPath] = module
}

步骤四:编译执行

function myRequire(filename){
    // ...
    // 4.执行加载(编译执行)
    module.load()
    // 返回数据
    return module.exports
}
Module.prototype.load = function(){
    //通过步骤3创建的实例,this.id拿到absPath,通过Module._extensions对应键处理函数
    let extname = path.extname(this.id)
    //将this,即module传入处理函数
    Module._extensions[extname](this)
}

进而去完善Module._extensions

Module._extensions = {
    '.js'(module){
        // 读取并包装函数
        let content = Module.wrapper[0] + fs.readFileSync(module.id, 'utf8') + Module.wrapper[1]
        // vm将字符串解析为可执行函数
        let compileFn = vm.runInThisContext(content)
        // 准备参数
        let exports = module.exports
        let dirname = path.dirname(module.id)
        let filename = module.id
        // 调用(此处call绑定exports,也说明了为什么每个模块打印this时是{})
        compileFn.call(exports, exports, myRequire, module, filename, dirname)
    },
    '.json'(module){
        let content = fs.readFileSync(module.id, 'utf8')
        module.exports = JSON.parse(content)
    }
}

一个自执行函数的结构,并将模块内可调用的属性或对象写入参数

Module.wrapper = [
    '(function(exports, require, module, __filename, __dirname){',
    '})'
]

代码完整版

const fs = require('fs')
const path = require('path')
const vm = require('vm')    //将字符串处理成可执行代码,类似eval

class Module{
    constructor(id){
        this.id = id       //id为模块标识
        this.exports = {}  //未来导出数据的容器
    }
}
Module._resolveFilename = function (filename){
    // 使用path将filename转为绝对路径
    let absPath = path.resolve(__dirname, filename)
    //判断当前路径对应内容是否存在
    if(fs.existsSync(absPath)){
        // 存在则直接返回
        return absPath
    }else{
        // 不存在则进行文件定位,对文件进行后缀补足,再查询文件是否存在
        let suffix = Object.keys(Module._extensions)
        console.log(suffix)  // ['.js', '.json']
        for (let index = 0; index < suffix.length; index++) {
            let path = absPath + suffix[index]
            if(fs.existsSync(path)){
                return path
            }
        }
    }
    throw new Error(`${filename} is not exists`)
}
Module._cache = {}
Module.wrapper = [
    '(function(exports, require, module, __filename, __dirname){',
    '})'
]
Module.prototype.load = function(){
    //通过步骤3创建的实例,this.id拿到absPath,通过Module._extensions对应键处理函数
    let extname = path.extname(this.id)
    //将this,即Module传入处理函数
    Module._extensions[extname](this)
}
Module._extensions = {
    '.js'(module){
        // 读取并包装函数
        let content = Module.wrapper[0] + fs.readFileSync(module.id, 'utf8') + Module.wrapper[1]
        // vm将字符串解析为可执行函数
        let compileFn = vm.runInThisContext(content)
        // 准备参数
        let exports = module.exports
        let dirname = path.dirname(module.id)
        let filename = module.id
        // 调用(此处call绑定exports,也说明了为什么每个模块打印this时是{})
        compileFn.call(exports, exports, myRequire, module, filename, dirname)
    },
    '.json'(module){
        let content = fs.readFileSync(module.id, 'utf8')
        module.exports = JSON.parse(content)
    }
}

function myRequire(filename){
    // 1:获取绝对路径
    let absPath = Module._resolveFilename(filename)
    // 2.缓存优先
    let cache = Module._cache[absPath]
    if(cache) return cache.exports
    // 3.创建空对象加载目标模块
    let module = new Module(absPath)
    // 写入缓存
    Module._cache[absPath] = module
    // 4.执行加载(编译执行)
    module.load()
    // 返回数据
    return module.exports
}

let str = myRequire('./test')
console.log('str', str)

let json = myRequire('./testJ')
console.log('json', json.name)