为什么从“..”父索引文件的相对导入会导致异步模块解析行为?

Why do relative imports from a '..' parent index file cause asynchronous module resolution behaviour?

我有一个结构如下的回购协议:

src/ 
   index.ts
   main.ts
   bar/
      file.ts
      index.ts
   foo/
      fooFile.ts

src/index 只是一个顶级索引文件,可以导出我包中的所有内容。

但是,我要追踪的行为实际上需要我调用此文件中的某些功能,稍后我会详细介绍。

src/bar/file.ts 导出纯字符串。

src/foo/fooFile.ts 从父 .. 索引导入该字符串。

// comments relate to what happens if you run `node lib/index.js`
import { fileName } from "..";
import {fileName as fileName2} from "../bar"; 

export const test = "test"; 

const myData = {
    data: fileName,  // This resolves to undefined, 
    data2: fileName2 //This resolves to "bar"
}; 


export function main() {
    console.log(myData); 
    console.log(fileName);  // This resolves to "bar"
}

如果我的 src/index.ts 看起来像:

import {main} from "./foo/fooFile";
export * from "./bar"; 
export * from "./foo/fooFile"; 

main();

然后我们得到这种异步行为 - 其中从 .. 导入 fileName 在声明 myData const 时解析为未定义,但在运行时解析字符串。

而如果我从 ../bar 导入,那么我在两个实例中都得到了字符串。

即。输出:

{ data: undefined, data2: 'bar' }
bar

但是,这种行为似乎只在我从索引文件调用 main() 函数时才会发生。如果我在 main.ts

中做同样的事情
import {main} from "./index";

main();

我不明白这种行为。

{ data: 'bar', data2: 'bar' }
bar

我想这种行为的原因与节点模块解析和循环依赖有关 - 谁能准确解释为什么会出现这种行为?

此处的回购:https://github.com/dwjohnston/import-from-parent-issue

请注意,我使用 TypeScript 创建了此重现 - 我想这不是问题的原因 - 但这是重现我实际面临的问题的最佳方式。

你说得对,这是因为循环依赖。

那么第一种情况会发生什么:

  1. 开始处理 index.ts
  2. index.ts 进口 foo/fooFile.ts
  3. foo/fooFile.ts 的处理已开始
  4. foo/fooFile.ts 进口 index.ts
  5. index.ts 已经被处理,在 nodejs 而不是 export blabla 中,您将使用 exports 对象并为其分配属性,然后该对象将从 require 当你 require 这个文件在某处时的功能。如果存在循环依赖,则返回“未完成”exports 对象,因此它会获得已经分配给它的属性,但是 nodejs 不会尝试继续执行 exports 这个对象到的文件获取由于循环依赖而丢失的属性。所以本例中的“未完成”exports 对象只是一个空对象,因为我们没有导出任何东西。这意味着如果您在 foo/fooFile.ts 中执行 import * as obj from '..',则此 obj 将只是一个空对象。由于您导入 { fooFile }fooFile 变为未定义

剩下的过程应该很清楚了,当main函数触发时,index.ts已经导出了一些变量,包括file变量,所以main 功能可以使用它。

现在是第二种情况。我想看到这个问题我应该删除 index.ts 中的 main() 行,因为如果我不这样做 - 我没有观察到你在说什么,它仍然显示 { data: undefined, data2: 'bar' }.

所以我删除了 index.ts 中的 main() 行。看起来一切都应该是一样的,因为无论如何你导入相同的 index.ts 文件,如果不是因为一个小问题,它实际上会是这样:

Note that I've created this repro using TypeScript - I imagine that this isn't the cause of the issue

实际上有点像。如果你在这两种情况下编译这个项目并打开 index.ts 文件,你会看到这个。第一种情况:

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
var fooFile_1 = require("./foo/fooFile");
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
fooFile_1.main();

第二种情况,当我注释掉main():

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
// main()

注意到区别了吗?如果删除 main(),那么 index.ts 中的第一个导入,即 import { main } from './foo/fooFile' 就没有用了。通常打字稿不会删除它,因为即使 import 没用,它也可能会产生一些副作用,但在这里你无论如何都要从 fooFile 稍后导入一些东西:export * from './foo/fooFile'。好吧,不是导入而是再导出,但是在 nodejs 的上下文中没有任何区别。

所以现在一切都开始变得有意义了:

  1. 处理 main.ts 已启动,它导入 index.ts
  2. 处理 index.ts 已开始
  3. index.ts 导入 ./bar,获取 fileName 并导出它。现在这个“未完成的”exports 对象不是空的,它包含 { fileName: 'bar' }
  4. index.ts 进口 ./foo/fooFile
  5. ./foo/fooFile 导入 index.ts,但它已经导出 fileName 因此可以使用

剩下的应该清楚了

您可以向自己保证这就是实际发生的情况,只是改变了导入的顺序。要么在开头手动添加一行 require('./foo/fooFile') 到已编译的 index.js 文件,这样您将像第一种情况一样保留导入顺序,并且什么都不会改变。或者在 index.ts 中交换 ./bar./foo/fooFile 导入,那将是相同的。

这部分是由 typescript 引起的,因为如果你只是 运行 使用 nodejs 的这些文件,它不会删除顶部未使用的导入并且不会再次发生任何变化