Nodejs

NodeJS Require

相关源码对应:

NodeJS Require

相关源码对应:

官方文档见:

功能简介

NodeJS 导入模块时有主要两个地方要注意:如何解析模块地址以及如何改善 IO 操作消耗大量的性能。

  • 地址解析:在 require 时,我们可以传入 fs、path、a/index.js、webpack 等各种参数,解析分两种情况:如果传入核心部件名或是相对路径名称,则直接导入模块;不然则从本目录开始寻找 node_modules 目录,没找到时递归回退直到系统根目录。
  • 性能消耗:所有加载过的模块都用 WeakMap 缓存下来,再次加载时,直接返回已经加载好的内容。

代码解析

模块初始化

模块实例上保存有 id、path、filename、loaded、children、exports 等信息,各有用处。

function Module(id = '') {
    this.id = id
    this.path = path.dirname(id)
    this.exports = {}
    this.filename = null
    this.loaded = false
    this.children = []
}

模块包装

模块引入本质上是调用 Require 函数去加载一段代码,并引入 exports 的结果。模块并不能直接引入,在初始化的时候,会通过 Wrapper 进行包装,也就是:

// 传入了 exports、require、module、__filename、__dirname 这几个参数
(function(exports, require, module, __filename, __dirname) {
    /* 模块源码 */
})

源码中,使用数组 wrapper 定义了包装相关的代码,使用 wrap 函数定义了包裹动作,也就是给 xxx.js 简单的加上头部尾部。最后,通过一个 proxy 保护源对象,将可能的修改记录到 patched 变量。

let patched = false
let wrap = function(script) {
    return Module.wrapper[0] + script + Module.wrapper[1]
}
const wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ', 
    '\n});'
]
// 修改 wrapper 时(set 或 Object.defineProperty),会被 patched 记录
let wrapperProxy = new Proxy(wrapper, {
    set(target, property, value, receiver) {
        patched = true
        return ReflectSet(target, property, value, receiver)
    },
    defineProperty(target, property, descriptor) {
        patched = true
        return ObjectDefineProperty(target, property, descriptor)
    }
})
ObjectDefineProperty(Module, 'wrap', {
    get() {
        return wrap
    },
    set(value) {
        patched = true
        wrap = value
    }
})
ObjectDefineProperty(Module, 'wrapper', {
    get() {
        return wrapperProxy
    },
    set(value) {
        patched = true
        wrapperProxy = value
    }
})

TODO 修改 wrapper 导致 patched 变动带来的影响

数据缓存

module.parent

module.parent 属性用来表示第一次引入某模块的模块。比如,当 parent 为 null 时,就说明此模块不被其它模块引入,所以是执行程序的入口。我们来看看它的实现。

// 首先定义了一个缓存 Map::moduleParentCache,用作缓存
const moduleParentCache = new SafeWeakMap()

function Module(id = '', parent) {
    // 实例化 Module 时,会将 this 的 parent 存到缓存中
    moduleParentCache.set(this, parent)
}

// 定义了取 parent 属性对应的方法
function getModuleParent() {
    return moduleParentCache.get(this)
}
ObjectDefineProperty(Module.prototype, 'parent', {
    get: pendingDeprecation
        ? deprecate(
              getModuleParent,
              'module.parent is deprecated due to accuracy issues. Please use ' +
                  'require.main to find program entry point instead.',
              'DEP0144'
          )
        : getModuleParent
})
内置对象

内置对象即 http、path 等模块。

[
  "assert",        "async_hooks",    "buffer",
  "child_process", "cluster",        "console",
  "constants",     "crypto",         "dgram",
  "dns",           "domain",         "events",
  "fs",            "http",           "http2",
  "https",         "inspector",      "module",
  "net",           "os",             "path",
  "perf_hooks",    "process",        "punycode",
  "querystring",   "readline",       "repl",
  "stream",        "string_decoder", "timers",
  "tls",           "trace_events",   "tty",
  "url",           "util",           "v8",
  "vm",            "worker_threads", "zlib"
]
const builtinModules = []
for (const [id, mod] of NativeModule.map) {
    if (mod.canBeRequiredByUsers) {
        builtinModules.push(id)
    }
}

ObjectFreeze(builtinModules)
Module.builtinModules = builtinModules

模块导入

自动后缀补全

导入一个模块时,如果模块名不带后缀,则会自动匹配其后缀。比如,require('a'),会自动匹配 'a','a.xxx','a/index.xxx'。

读取目录('a')听起来有些奇怪。其实主要逻辑一说就清楚了:如果目录下定义了 package.json 并在其中指定了 main 属性,便直接加载 main 指向的文件。

// 获取 package.json 的 main 属性
function readPackageMain(requestPath) {
    const pkg = readPackage(requestPath)
    return pkg ? pkg.main : undefined
}

// 用作缓存已经加载过的 packageJSON
const packageJsonCache = new SafeMap()

// 读取 packageJSON
function readPackage(requestPath) {
    const jsonPath = path.resolve(requestPath, 'package.json')

    // 缓存中有,就从缓存中取
    const existing = packageJsonCache.get(jsonPath)
    if (existing !== undefined) return existing

    // 读取 packageJSON
    // path.toNamespacedPath 可不用理会,他相关 Windows 系统下的路径名前缀,和 Loader 本身无关系
    const result = packageJsonReader.read(path.toNamespacedPath(jsonPath))
    const json = result.containsKeys === false ? '{}' : result.string
    if (json === undefined) {
        packageJsonCache.set(jsonPath, false)
        return false
    }

    // 解析 json 并添加至缓存
    try {
        const parsed = JSONParse(json)
        const filtered = {
            name: parsed.name,
            main: parsed.main,
            exports: parsed.exports,
            imports: parsed.imports,
            type: parsed.type
        }
        packageJsonCache.set(jsonPath, filtered)
        return filtered
    } catch (e) {
        e.path = jsonPath
        e.message = 'Error parsing ' + jsonPath + ': ' + e.message
        throw e
    }
}

尝试依次读取 'a','a.xxx','a/index.xxx':

function tryPackage(requestPath, exts, isMain, originalPath) {
    const pkg = readPackageMain(requestPath)

    // 目录下不包含 package.json 则读取 index.xxx 文件
    if (!pkg) {
        return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain)
    }

    const filename = path.resolve(requestPath, pkg)
    // 依次读取 'a','a.xxx','a/index.xxx'
    let actual =
        tryFile(filename, isMain) ||
        tryExtensions(filename, exts, isMain) ||
        tryExtensions(path.resolve(filename, 'index'), exts, isMain)

    // 如果上述逻辑读取失败了(比如 main 指向一个错误文件),回退为读取源目录下的 index.xxx 文件
    if (actual === false) {
        actual = tryExtensions(path.resolve(requestPath, 'index'), exts, isMain)

        // 报错:未找到 xxx 模块
        if (!actual) {
            const err = new Error(
                `Cannot find module '${filename}'. ` + 'Please verify that the package.json has a valid "main" entry'
            )
            err.code = 'MODULE_NOT_FOUND'
            err.path = path.resolve(requestPath, 'package.json')
            err.requestPath = originalPath
            throw err
        } else if (pendingDeprecation) {
            const jsonPath = path.resolve(requestPath, 'package.json')
            process.emitWarning(
                `Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` +
                    'Please either fix that or report it to the module author',
                'DeprecationWarning',
                'DEP0128'
            )
        }
    }

    // 没有报错则正常返回,表示 tryPackage 成功
    return actual
}

文件预判

在读取某个模块并处理之前,我们得先确定该文件的存在,并读取该文件。就像 tryPackage 函数中的 tryFile、tryExtensions 等函数,就是读取某个目录(下的某文件)。

// 默认依次尝试 xxx.js,xxx.json,xxx.node 三种后缀
function tryExtensions(p, exts, isMain) {
    for (let i = 0; i < exts.length; i++) {
        const filename = tryFile(p + exts[i], isMain)
        if (filename) {
            return filename
        }
    }
    return false
}

// 判断文件是否存在
function tryFile(requestPath, isMain) {
    // 首先尝试从缓存中读取
    const rc = stat(requestPath)
    if (rc !== 0) return

    // 根据是否开启保留符号链接选项,返回不同地址的解析。
    if (preserveSymlinks && !isMain) {
        return path.resolve(requestPath)
    }
    return toRealPath(requestPath)
}

// 通过 stat 函数来缓存判断文件是否存在。
function stat(filename) {
    filename = path.toNamespacedPath(filename)
    if (statCache !== null) {
        const result = statCache.get(filename)
        if (result !== undefined) return result
    }
    const result = internalModuleStat(filename)
    if (statCache !== null) statCache.set(filename, result)
    return result
}

TODO realpathCache

// IO 操作消耗巨多性能,所以所有路径读取操作都会使用一个对象作缓存
const realpathCache = new Map()

模块载入

模块载入的缓存判定大致如下:

  1. 如果已经载入模块,直接返回模块的导出内容
  2. 读取文件,创造一个新的 module 实例保存到缓存,然后载入文件内容并返回导出结果。

模块载入需要调用原始模块的 require 方法,而实际上 module.require 方法也提供了一种载入的方法,和 require 类似。

// 用来维护依赖层级
let requireDepth = 0

Module.prototype.require = function(id) {
    validateString(id, 'id')
    if (id === '') {
        throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string')
    }
    requireDepth++
    try {
        return Module._load(id, this, /* isMain */ false)
    } finally {
        requireDepth--
    }
}

从前面几个小节可以看到,代码的各个细节都充满了缓存对象。module.require 也不例外,不仅缓存了模块和文件名的对应关系,还缓存了模块与载入模块间相对路径的对应关系。

// 模块的相对路径关系的缓存
const relativeResolveCache = ObjectCreate(null)

Module._load = function(request, parent, isMain) {    
    let relResolveCacheIdentifier
    // 带 parent 的模块走特殊的缓存处理
    if (parent) {
        // 缓存的属性名为 parent 路径 + \x00 + 请求路径
        relResolveCacheIdentifier = `${parent.path}\x00${request}`
        const filename = relativeResolveCache[relResolveCacheIdentifier]
        if (filename !== undefined) {
            const cachedModule = Module._cache[filename]
            if (cachedModule !== undefined) {
                // 将模块添加到 parent.children
                updateChildren(parent, cachedModule, true)
                // 如果模块仍未载入,需要处理循环引用的情况(如 a.js 引用 b.js,b.js 又引用 a.js)
                if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule)
                // 否则直接返回 exports 内容
                return cachedModule.exports
            }
            // TODO 删除缓存?
            delete relativeResolveCache[relResolveCacheIdentifier]
        }
    }

    // TODO Module._resolveFilename
    const filename = Module._resolveFilename(request, parent, isMain)

    // 命中缓存
    const cachedModule = Module._cache[filename]
    if (cachedModule !== undefined) {
        updateChildren(parent, cachedModule, true)
        if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule)
        return cachedModule.exports
    }

    // 若载入内部对象(如 a = require('http'))
    const mod = loadNativeModule(filename, request)
    if (mod && mod.canBeRequiredByUsers) return mod.exports

    // 创造新的 module 实例
    const module = new Module(filename, parent)

    // 主入口文件相关逻辑
    if (isMain) {
        process.mainModule = module
        module.id = '.'
    }

    // 缓存文件名与模块的关系以及模块的相对路径关系
    Module._cache[filename] = module
    if (parent !== undefined) {
        relativeResolveCache[relResolveCacheIdentifier] = filename
    }

    let threw = true
    try {
        // 若开启了 getSourceMapsEnabled,那么错误实例会取代模块实例被缓存下来
        if (getSourceMapsEnabled()) {
            try {
                module.load(filename)
            } catch (err) {
                rekeySourceMap(Module._cache[filename], err)
                throw err /* node-do-not-add-exception-line */
            }
        } else {
            module.load(filename)
        }
        threw = false
    } finally {
        // 若模块载入出错
        if (threw) {
            // 释放模块实例的缓存
            delete Module._cache[filename]
            // 同时清空模块的 children
            if (parent !== undefined) {
                delete relativeResolveCache[relResolveCacheIdentifier]
                const children = parent && parent.children
                if (ArrayIsArray(children)) {
                    const index = children.indexOf(module)
                    if (index !== -1) {
                        children.splice(index, 1)
                    }
                }
            }
        } 
        // 当模块载入成功,则设置 module.exports 的 __proto__
        else if (
            module.exports &&
            // proxy 对象是没有 prototype 的内置对象,需要特殊处理
            !isProxy(module.exports) &&
            ObjectGetPrototypeOf(module.exports) === CircularRequirePrototypeWarningProxy
        ) {
            ObjectSetPrototypeOf(module.exports, PublicObjectPrototype)
        }
    }

    return module.exports
}

不同后缀文件,对应不同的载入逻辑。

Module._extensions['.js'] = function(module, filename) {
    if (filename.endsWith('.js')) {
        const pkg = readPackageScope(filename)
        // 如果是 ES Module,则报错
        if (pkg && pkg.data && pkg.data.type === 'module') {
            const parent = moduleParentCache.get(module)
            const parentPath = parent && parent.filename
            const packageJsonPath = path.resolve(pkg.path, 'package.json')
            throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath)
        }
    }

    // 读取文件内容
    const content = fs.readFileSync(filename, 'utf8')
    // 模块编译
    module._compile(content, filename)
}

Module._extensions['.json'] = function(module, filename) {
    const content = fs.readFileSync(filename, 'utf8')
    try {
        // BOM 即 字节序,在 UTF-8 的文件中并不需要这么一个玩意儿,所以解析字符串前先把他去掉
        // JSON 文件直接通过 JSONParse 函数解析
        module.exports = JSONParse(stripBOM(content))
    } catch (err) {
        err.message = filename + ': ' + err.message
        throw err
    }
}

模块编译

模块编译,即向模块注入 exports、__filename 等变量,并在指定上下文中运行模块的代码。可以发现,之前小节提到的模块包装是其中一个步骤。

Module.prototype._compile = function(content, filename) {
    let moduleURL
    let redirects

    // 包装当前模块内容
    const compiledWrapper = wrapSafe(filename, content, this)

    let inspectorWrapper = null
    if (getOptionValue('--inspect-brk') && process._eval == null) {
        if (!resolvedArgv) {
            // We enter the repl if we're not given a filename argument.
            if (process.argv[1]) {
                try {
                    resolvedArgv = Module._resolveFilename(process.argv[1], null, false)
                } catch {
                    // We only expect this codepath to be reached in the case of a
                    // preloaded module (it will fail earlier with the main entry)
                    assert(ArrayIsArray(getOptionValue('--require')))
                }
            } else {
                resolvedArgv = 'repl'
            }
        }

        // Set breakpoint on module start
        if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
            hasPausedEntry = true
            inspectorWrapper = internalBinding('inspector').callAndPauseOnStart
        }
    }

    // 模块所在文件夹
    const dirname = path.dirname(filename)
    const require = makeRequireFunction(this, redirects)
    let result
    const exports = this.exports
    const thisValue = exports
    const module = this

    // 缓存,用来保存模块内部的模块引用关系
    if (requireDepth === 0) statCache = new Map()

    if (inspectorWrapper) {
        result = inspectorWrapper(compiledWrapper, thisValue, exports, require, module, filename, dirname)
    } else {
        result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname)
    }

    // 一个全局标记,用来标记是否加载过用户 CommonJS 模块
    hasLoadedAnyUserCJSModule = true

    if (requireDepth === 0) statCache = null
    return result
}

相关命令行参数

pendingDeprecation

用来判断是否打开命令行参数:--pending-deprecation。打开时,使用部分待弃用的 API 会报警告。

const pendingDeprecation = getOptionValue('--pending-deprecation')

用来判断是否打开命令行参数:--preserve-symlinks。用来标志当解析路径时,是否需要将符号链接替换为真实路径。

const preserveSymlinks = getOptionValue('--preserve-symlinks')

Copyright © 2024 Lionad - CC-BY-NC-CD-4.0