Three.js: 循环依赖

创建于 2015-03-16  ·  81评论  ·  资料来源: mrdoob/three.js

嘿大家。

@kumavis和我一直在努力寻找一种有效的方法将 THREE.js 迁移到 browserify 架构。 我们取得了不错的进展,甚至可以将所有文件转移到 browserify 构建系统并能够使用 gulp 生成 three.min.js。

不幸的是,这些示例不起作用,因为与 commonjs 不同,browserify 无法处理循环依赖,其中在 THREE.js 中有很多。

我已经制作了一个交互式图表来描述这里的依赖关系。

除非并且直到这些问题得到解决,否则我们将无法将 THREE.js 转移到 browserify 构建中。

我不认为这是 browserify 的不足,而是 THREE.js 的问题。 一般来说,循环依赖在软件中是一件坏事,会导致各种问题。

Suggestion

最有用的评论

@Mugen87哇,5 年了! 恭喜你终于成功了:fire::clap:
那时我在制作这些图表时玩得很开心 :smile_cat:

所有81条评论

这真是一个解开的结
http://jsbin.com/medezu/2/edit?html ,js,输出
image

@coballast你可以发布你用来生成依赖json的代码吗?

直接使用预编译好的three.min.js文件即可。 没有必要在 Browserfy 中将 Three.js 分解成单独的文件,你只是让你的生活变得更加困难而没有真正的好处。

我根据经验说话,因为我们使用了 three.js 的 npm 模块,它工作得很好。 我们只是将它打包为一个文件,并将其包装在一个 CommonJS 样式的模块中。 这种方法适用于 browserfy,我理解很多人已经在这样做了。

这个用例不需要解开这个结。

@kumavis我刚刚转储了依赖结构。 然后下面的代码生成了图表:

var fs = require('fs-extra');
var unique = require('uniq');
var util = require('util');

function getRequiredObjects(dependencies){
  var result = [];
  for(var i = 0; i < dependencies.usedObjects.length; i++){
    var object = dependencies.usedObjects[i];
    if(dependencies.definedObjects.indexOf(object) == -1)
      result.push(object);
  }

  return result;
};

var dependencies = JSON.parse(fs.readFileSync('./dependencies.json'));

var objects = [];
for(var f in dependencies){
  objects = objects.concat(dependencies[f].usedObjects);
  objects = objects.concat(dependencies[f].definedObjects);
}

objects = unique(objects);


var nodes = objects.map(function(o){
  return {data: {id: o} };
});

var edges = [];
for(var f in dependencies){
  var dependency = dependencies[f];
  var requiredObjects = getRequiredObjects(dependency);
  for(var j = 0; j < dependency.definedObjects.length; j++){
    for(var k = 0; k < requiredObjects.length; k++){
      edges.push({ data: { source: dependency.definedObjects[j], target: requiredObjects[k] } });
    }
  }
}

var graph = {nodes: nodes, edges: edges};

var eliminateImpossibleCycleNodes = function(graph){
  graph.nodes = graph.nodes.filter(function(node){
    var source_edge = null;
    var dest_edge = null;
    for(var i = 0; i < graph.edges.length; i++){
      if(graph.edges[i].data.source == node.data.id)
        source_edge = graph.edges[i];
      if(graph.edges[i].data.target == node.data.id)
        dest_edge = graph.edges[i];
    }

    if(source_edge != null && dest_edge != null)
      return true;
    else
      return false;
  });

  graph.edges = graph.edges.filter(function(edge){
    var source_exists = false, target_exists = false;
    for(var i = 0; i < graph.nodes.length; i++){
      if(edge.data.source == graph.nodes[i].data.id)
        source_exists = true;
      if(edge.data.target == graph.nodes[i].data.id)
        target_exists = true;
    }

    return source_exists && target_exists;
  });
};

for(var i = 0; i < 500; i++)
  eliminateImpossibleCycleNodes(graph)


console.log(JSON.stringify(graph));

@bhouston更多关于three.js 代码库的健康状况

我只知道在我提供了一些帮助的数学库中,循环依赖是所有语言的规范。 因为 Matrix4 上的函数可能以 Vector3 作为参数,而 Vector3 可能可以被 Matrix4 转换。 在数学库中以一种方式制作所有依赖项会使库的该部分使用起来很烦人。

现在我确实主张数学库不知道该库的任何其他部分——更复杂的类型不应该真正泄漏到该模块中。 因此,从这个意义上说,我主张尝试减少模块间的循环依赖,而不是删除模块内各个文件之间的所有循环依赖。

这是一个说明微妙复杂性的案例。 需要明确的是,这里我不是批评实现本身,而是批评副作用。

Vector3Matrix4形成循环依赖,因为它们公开了一系列函数,这些函数相互使用作为输入或输出类型。 两者都使用 Three.js 通用的样式实现,通过 IIFE 定义函数以包含用于执行计算的临时变量。

作为函数定义的一部分, Matrix4#lookAt能够立即实例化划痕。

lookAt: function () {

  var x = new THREE.Vector3();
  var y = new THREE.Vector3();
  var z = new THREE.Vector3();

  return function ( eye, target, up ) {
    /* ... */

但是, Vector3#project必须在第一次运行时实例化划痕。

project: function () {

  var matrix;

  return function ( camera ) {

    if ( matrix === undefined ) matrix = new THREE.Matrix4();

    /* ... */

为什么? 因为在定义Class的时候,并不是所有的Class都已经定义好了。 定义Vector3时, Matrix4尚不存在。 现在,临时变量的实际实例化时间并不重要。 这里真正的收获是,当前的实现取决于构建系统将文件连接在一起的顺序。 这是一个非常遥远的耦合,对构建系统的更改或以改变连接顺序的方式重命名文件可能导致无效的构建,没有明显的联系。

这只是这个结体现为虫子的方式之一。 然而,虽然我们可以解决这个特定问题,但我没有一个不需要对 API 进行大量重大更改的通用解决方案。

嗯...我查看了 ILM 的 C++ 数学库,我认为它是数学库方面的黄金标准。 令人惊讶的是,它们不是循环的。 他们基本上有一个定义明确的从简单到复杂类型的顺序,我猜如果你非常小心地这样做,它会起作用:

https://github.com/openexr/openexr/tree/master/IlmBase/Imath

Math 然后 Vec 似乎是最简单的。

对依赖图的进一步观察:

Material s 的基类有两个方向的 deps
image
有点难看,但Geometry s 似乎在基类上有很好的单向依赖
image
Light s 和Camera s 有类似的情况——就它们的基类而言很好,但是Object3D对它们的依赖似乎是不必要的。
image
image
Curve s Path s Line s 看起来不错,但是Shape有点纠结。
image

@coballast谢谢! 这是伟大的洞察力。

添加我自己的评论:)

顺便说一句,例如,我已经查看了 Material 依赖于 MeshDepthMaterial 的方式。 很简单

if ( this instanceof THREE.MeshDepthMaterial )

更改为微不足道

if ( this.type == 'MeshDepthMaterial' )

瞧 - 没有依赖。 我猜这个可怕的图表有一半是同一级别的问题。

有趣的是,这种依赖发生在单个 toJSON 方法中。 我的意思是,难道不能直接在 MeshDepthMaterial 中替换它吗? 就像是

THREE.MeshDepthMaterial.prototype.toJSON =  function () {
    var output = THREE.Material.prototype.toJSON.apply(this);
    if ( this.blending !== THREE.NormalBlending ) output.blending = this.blending;
    if ( this.side !== THREE.FrontSide ) output.side = this.side;

@makc通常,我们在instanceof的任何地方都应该将该代码移至特定的类本身。 这将有助于消除很多结。

只是想说虽然 AMD 不支持循环引用,但 ES6 模块支持https://github.com/ModuleLoader/es6-module-loader/wiki/Circular-References-&-Bindings

我只是想知道除了依赖解决问题(可以在模块系统加载器的实现中解决,例如system.js ),three.js 中的循环引用会产生哪些问题?

幸运的是,看起来我们可以分阶段进行攻击。 我认为在以前的版本中已经进行了很多 api 重大更改,所以我不确定这是不可能的。

对于instanceof情况(可能是大多数情况),它们应该能够在不进行重大更改的情况下得到解决。

我也在这里订阅。 让我们一步一步来。
我同意我们应该删除所有不必要的循环依赖,比如材料之一。
我也同意@bhouston的观点,即数学库非常依赖于彼此,因为交互是使数学库有用的原因。

有人能画出简单的吗?? 如果它不妨碍库,那么减少循环依赖总是一个好主意。 我们稍后可以看看如何处理其他人。

@zz85我也遇到了循环依赖的问题。 当我们尝试在循环引用文件中预先创建某些对象时,这主要是一个问题。

6252 应该清除MaterialObject3D上的很多循环依赖。

这就是Mesh的样子。 也许是一些无关紧要的部门,但不要太疯狂。
image

带有Object3DGeometry的圆形。 Object3D -> Mesh参考在上面的 PR 中得到解决。 Mesh -> Geometry引用很好,b/c Mesh控制Geometry的实例。 它仍然可以中断,因为它对特定于类的行为进行类型检查( Geometry / BufferGeometry )。

至于Geometry -> Mesh参考,是提供geometry.mergeMesh( mesh )Geometry是比Mesh更低级别的概念,所以我会将其反转为mesh.mergeIntoGeometry( geo )并弃用mergeMesh

如果有人获得了修复其中一些问题的 pr 合并,请告诉我,我将更新图表以反映当前的事态。

@bhouston @gero3我不相信需要循环依赖才能为数学库获得相同级别的可用性/实用性。 我可能错了,但我们不能让 Vector3 完全隔离/不了解系统的其余部分,并修改它的原型以适应 Matrix4 模块中的 Matrix4 吗? 这在概念上对我来说很有意义,因为矩阵比向量更复杂。 我认为最好有一个定义明确的顺序来构造原型和类以防止发生意外。

@bhouston @gero3我认为我们完全可以在不更改 api 的情况下做到这一点。 我会四处看看,看看是什么。

关于数学的事情,我猜你可以把所有的“便笺簿”放在一个地方。 但我敢打赌,不会有一个没有 Vector3 和 Matrix4 的可用 3js 图

如果有一个解决方案不会改变数学库的性能或 API,我完全赞成。

@coballast无法在没有 API 更改的情况下删除周期性 dep,b/c 都提供使用其他类型的方法。 Vector3 Matrix4

至于browserify compat,我们唯一的要求是将临时变量上的实例化移出类定义时间(让它们在第一次运行时实例化)。 像这样偷懒。 这对 API 或性能没有影响。

我认为这些类型的变化是可以的。

@kumavis啊! 是的。 好的,我现在明白了。 这很容易。

我完全支持将 THREE 分解为具有 require 结构的较小模块,原因如下:

  1. 三是非常大的。 如果用户只需要他们需要的东西,它可能会减少客户端构建大小。 例如react-bootstrap可以让你做var Alert = require('react-bootstrap/lib/Alert');之类的事情,它不会捆绑每个引导模块。
  2. 有一些“插件”,比如OrbitControls.js修改三个全局对象本身,将自己放在THREE.OrbitControls上。 这是现代 javascript 框架中的一种反模式,因为它要求在构建过程中的全局命名空间中存在三个,而不是需要它的文件所要求的。 THREE 在内部也这样做,总是修改 THREE 命名空间,这对于包含特定的 THREE 模块并不理想。

将自己置于 THREE.OrbitControls 上

但是 3js 中的每一段代码都是这样吗?

@DelvarWorld写道:

我完全支持将 THREE 分解为具有 require 结构的较小模块,原因如下:

我曾经认为拆分它是一个好主意,但是对于 ThreeJS 来说,它现在很简单。 它对于那些刚接触 3D 的人来说更有用,因为它现在的形式是开发团队的优先事项。 您可以使用 ThreeJS 而不需要模块系统(其中有很多,并且它们并不完全兼容。)

@makc一样,我也对@DelvarWorld不将东西放在三个命名空间中的建议感到困惑。

相反,他们会在哪里/如何?

对我来说,只创建一个全局对象 THREE 似乎是一种很好的模式,它的所有部分(可能还有一些扩展/插件)都在其中。

我同意@DelvarWorld的观点,即 put-it-on-the-global 技术不利于代码库的健康——它是一种微妙的论点 b/c 将它放在全局本身不是问题,而是隐藏的依赖图,以及由于全局可用而产生的其他实践。

但这种说法主要局限于内部开发和代码结构。 至于将库作为静态代码包提供,将所有类放在全局 THREE 上对我来说很有意义。

一个反驳的论点是,在反序列化 THREE.js 场景 json 时,条目可以将它们的类列为可以从全局中提取的字符串,例如: THREE[ obj.type ] 。 这适用于不在标准 three.js 库中的类,只要您在反序列化之前在THREE上定义它们。 不确定如何最好地在没有THREE全局的情况下替换此行为。

这适用于不在标准 three.js 库中的类,只要您在反序列化之前在 THREE 上定义它们。 不确定如何最好地在没有三个全局的情况下替换此行为。

如果一切都是模块,您可以执行此模式(或它的某些变体):

var objectType = require( "THREE." + obj.type );

ES6 在模块方面有很多变化。 那时我会重新审视 ThreeJS 的模块化。

三的构建版本(人们可以手动下载的 javascript 文件)仍然会在三命名空间中包含所有内容。 您可以使用构建的入口点文件来执行此操作:

var THREE = {
    Geometry: require("./geometry"),

等,这对新手仍然有用,并且易于上手。

对于那些在现代 javascript 构建中使用 npm 和 requirejs/browserify/webpack 的三个,我们可以做类似的事情

var Scene = require("three/scene"),
     Camera = require("three/camera"),

等,这可能会减少客户端大小捆绑包中内置的三个大小。 我可能是错的,因为我不知道三个中有多少是“核心”。 但是,现在这是不可能的,因为三不使用 require 语句。

无论哪种方式,现代模式都是需要模块,而不是让你的所有代码修改另一个库(修改全局三,不好),你的代码是独立的和模块化的,并用require语句指定它需要什么,比如React 源代码

我不认为我试图为使用 require/module 语法做一个完整的论据会有帮助,因为网上有很多关于它的好资源。 但是鼓励其他人修改三个命名空间来添加像 OrbitControls 这样的插件是不好的。

请注意@DelvarWorld ES6 以非常具体和独特的语法将模块正式引入 JavaScript:

http://www.2ality.com/2014/09/es6-modules-final.html

@bhouston哦,是的,我不知道 require 与 import (导入可能是更好的选择),一般只支持模块模式。

@bhouston @DelvarWorld @kumavis我的一个长期项目是编写一个自动 es5 -> es6 转换器,它可以容纳和转换 commonjs/amd 模块到 es6,并希望使用类/生成器等 es6 结构识别和重写大量 javascript等等。 当浏览器赶上标准以准备消费代码时,可以使用像 es6ify 这样的 browserify 转换。 将 THREE 转移到仅在内部级别上的 browserify 是准备将其输入到此类工具的良好的第一步。

这与我(显然非常糟糕)试图提出的观点无关。 我想尽可能多地删除这些循环依赖,独立于任何模块化问题,因为我相信它会使 THREE 更稳定、更灵活,并且可能会消除许多错误作为令人愉快的副作用。

@coballast https://github.com/mrdoob/three.js/pull/6252已合并,应该会减少很多周期性依赖。 认为您可以生成新的深度图? 也许使它成为转换工具仓库中的实用程序

接下来是:在Vector3 Matrix4中创建临时变量,在第一次使用时延迟定义,而不是在定义时

有人愿意做志愿者吗? 应该很快

图表已更新。 http://jsbin.com/medezu/3/

这是一个屏幕截图:

snapshot3

我很高兴地报告,大量的 Object3Ds circ deps 已被淘汰。 干得好@kumavis!

哇,那是同一个代码库吗? 疯狂的

将致力于使图形生成成为实用程序的一部分。

仅检查图表, ShapeGeometry似乎是可以解开的类树。

@coballast认为你可以接受这个吗?

在第一次使用时懒惰地定义 Vector3 Matrix4 中的临时变量,而不是在定义时

这将是针对上游dev的 PR,而不是自动更改

另外我认为我们可以将问题标题从“严重的循环依赖问题”降级为“循环依赖”——情况有了很大改善!

@kumavis当然可以。 时间允许的时候会努力的。

这是我所看到的相互依赖关系清理的当前状态:
(箭头显示适当时应移除的连接)

  • [x] 材料
  • [x] 几何图形
  • [x] Object3D
  • [x] 数学
  • [x] 形状

    • [x] 形状 -> FontUtils

    • [x] 形状 -> ExtrudeGeometry

    • [x] 形状 -> ShapeGeometry

    • [x] 路径 -> 形状

  • [ ] 框 3

    • [] Box3 -> BufferGeometry

    • [] Box3 -> 几何

形状:

image

方框3:

image

数学:

这些节点是相互连接的,但通过它提供了足够的便利。
image

通过如下代码,Shape 似乎与ExtrudeGeometryShapeGeometry有依赖关系:

// Convenience method to return ExtrudeGeometry

THREE.Shape.prototype.extrude = function ( options ) {

  var extruded = new THREE.ExtrudeGeometry( this, options );
  return extruded;

};

// Convenience method to return ShapeGeometry

THREE.Shape.prototype.makeGeometry = function ( options ) {

  var geometry = new THREE.ShapeGeometry( this, options );
  return geometry;

};

现在看来ShapePath的子类,并且ExtrudeGeometryShapeGeometry都是Geometry的子类。 所以,你知道,你可以弄清楚在理想情况下哪些依赖项需要消失。

是的,这与Vector3 <-> Matrix4属于同一类别。 为方便起见,它们被链接在一起。 我认为这是一个坏主意,但不值得与之抗争。 我会将其标记为完成

Shape -> FontUtils可以通过使用诸如triangulate之类的移动方法移至更通用的 Utils 来删除。 但这样做并不是一个很大的胜利。 将其标记为完成。

Box3 -> BufferGeometryBox3 -> Geometry都可以被清理。

这是不将依赖于类的行为放在类本身上的另一种情况。

来源

setFromObject: function () {

  // Computes the world-axis-aligned bounding box of an object (including its children),
  // accounting for both the object's, and childrens', world transforms

  /* ... */

  if ( geometry instanceof THREE.Geometry ) {
    /* ... */
  } else if ( geometry instanceof THREE.BufferGeometry && geometry.attributes[ 'position' ] !== undefined ) {
    /* ... */
  }

  /* ... */

}

在这两种情况下,它只是试图遍历几何体的 worldCoordinate 顶点/位置。 我想知道在BufferGeometry上创建一个惰性vertices对象是否会大大简化代码,该对象在请求时查找值。 不确定性能影响。

或者,我们可以使用Geometry.computeBoundingBox
Geometry
BufferGeometry

运行 browserify 构建时,Box3 是一个问题点。 请参阅 coballast/threejs-browserify-conversion-utility#21 中的注释。

@kumavis您介意概述一下您推荐的处理 Box3/Geometry/BufferGeometry 的解决方案吗? 如果它很快,我可以实现它。

我现在看不到它,但我会从我上面的建议开始,使用在GeometryBufferGeometry上实现的geo.computeBoundingBox $ 代替 if/else这里. 相反, box3.setFromObj应该调用geometry.computeBoundingBox ,然后根据生成的 box3 设置参数。

这应该删除循环 deps 的Box3 -> BufferGeometryBox3 -> Geometry结尾。 如果我遗漏了什么,请告诉我。

嗯,结果代码可能有点复杂,这里到底有什么意义? Box3.setFromObject不应该存在,但这不是一个选项。 Geo's应该能够生产box3s,我对此没有问题。 是的,我猜Box3.setFromObject应该向 geo's 询问边界框/范围,但也许他们应该向Object3D / Mesh询问边界框/范围。

抱歉有点漫不经心。 让我知道你在想什么。

可能相关:#6546

如果没有这样的东西,就不可能从加载器脚本中分析这些动态依赖关系。

根据我的测试,循环依赖不是 commonJSification 的问题。 它们需要正确处理,正如之前在该线程中所述,它们使依赖关系图变得非常混乱,但它们不会阻止 THREE.js 在 commonJS 环境中工作(当然,在转换时)。

我刚刚使用我的三个 commonjs 转译器在 npm 上发布了一个完整的 commonjs 版本作为three.cjs

注意:为此,我必须在 master 上手动挑选 #6546。 虽然动态依赖在 node.js 中运行良好,但它们不能在 Browserify(或任何其他 cjs 到浏览器工具)中运行,因为它们需要执行静态依赖分析。

Browserify 证明: http ://requirebin.com/?gist=b7fe528d8059a7403960

@kamicane FYI -这里是 THREE 作为匿名函数的参数添加到Raycaster (以前是Ray )的地方。

我理解对匿名函数的需求(以防止浏览器中的全局泄漏),但是该参数是多余的,并且使整个文件属于计算依赖项的类别。 虽然可以通过 AST 修改动态删除参数,但它永远不会是一个防弹的解决方案(取决于写入参数的内容、从参数中读取的内容等)。 静态分析变得几乎不可能。 在这种情况下需要人工干预。

现在Raycaster更轻量级了,我们可以让它像其他类一样。

@mrdoob@Mugen87我们使用 Rollup 只使用我们真正需要的 Three.js 的部分。 当我们运行构建时,我们仍然会收到以下警告:

(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Matrix4.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Quaternion.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Sphere.js -> node_modules/three/src/math/Box3.js -> node_modules/three/src/math/Sphere.js
(!) Circular dependency: node_modules/three/src/objects/LineSegments.js -> node_modules/three/src/objects/Line.js -> node_modules/three/src/objects/LineSegments.js

Three.js 中是否仍然存在循环依赖关系,或者我们做错了什么?

Vector3 和 Matrix4 是相互绑定的,如果拉入一个,则需要拉入另一个。 技术上应该允许循环依赖。

@bhouston是的,我明白了,感谢您的提示。 是的,允许循环依赖,并且汇总没有问题,但我不确定循环依赖是否是一种好习惯。 Vector3仅取决于Matrix4因为multiplyMatricesgetInverse ,有关更多详细信息,请参阅(https://github.com/mrdoob/three.js/ blob/dev/src/math/Vector3.js#L315)

@roomle-build Idk,伙计,只是因为它明确引用了 Matrix4 构造函数? 关于什么

    applyMatrix4: function ( m ) {

        var x = this.x, y = this.y, z = this.z;
        var e = m.elements;

        var w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );

        this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
        this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
        this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;

        return this;

},

?

你可以说你可以通过 { elements: [....] } 并且它会起作用,但我们都知道它需要 Matrix4

让我们从Vector3开始。

Vector3取决于Matrix4因为projectunproject

    project: function () {

        var matrix = new Matrix4();

        return function project( camera ) {

            matrix.multiplyMatrices( camera.projectionMatrix, matrix.getInverse( camera.matrixWorld ) );
            return this.applyMatrix4( matrix );

        };

    }(),

    unproject: function () {

        var matrix = new Matrix4();

        return function unproject( camera ) {

            matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) );
            return this.applyMatrix4( matrix );

        };

    }(),

Vector3取决于Quaternion因为applyEulerapplyAxisAngle

    applyEuler: function () {

        var quaternion = new Quaternion();

        return function applyEuler( euler ) {

            if ( ! ( euler && euler.isEuler ) ) {

                console.error( 'THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.' );

            }

            return this.applyQuaternion( quaternion.setFromEuler( euler ) );

        };

    }(),

    applyAxisAngle: function () {

        var quaternion = new Quaternion();

        return function applyAxisAngle( axis, angle ) {

            return this.applyQuaternion( quaternion.setFromAxisAngle( axis, angle ) );

        };

    }(),

建议?

我不确定我们是否需要通过各种方式移除循环依赖。 但我可以想象将multiplyMatrices移动到Math模块。 然后签名当然会更改为multiplyMatrices( a: Matrix4, b: Matrix4, result: Matrix4 ): Matrix4 。 在Vector3中,您可以在Matrix4 import { multiplyMatrices } from './Math';中执行相同的操作(以保持Matrix4的 API 表面相同)。

我只是快速浏览了一下(没有查看Quaternian案例 - 仅Vec3/Mat4 ),我也不确定对其余代码库的性能影响和后果。 此外,我也不相信绝对有必要删除这些循环依赖项。 只是想分享我的想法,因为@mrdoob寻求建议

@roomle-build 所以基本上是为了避免循环依赖而创建更多模块,但是您仍然要使用所有这些模块吗? 如果每个数学方法都是它自己的模块,那么这可能会更有意义,那么你只引入你使用的那些,但这将是很多模块。

@makc不是真的。 这将是一个带有许多小“帮助”功能的大模块。 这也有助于摇树等。数学模块可能如下所示:

export const multiplyMatrices( a, b, result ) { // ... DO STUFF ... // }
export const getInverse( /* ... */ ) { // ... DO STUFF ... // }
// ...
// ...

消费模块会执行以下操作:

import { Matrix4 } from './Matrix4.js';
import { multiplyMatrices } from './math';
const result = new Matrix4( );
multiplyMatrices( a, b, result );

将所有内容捆绑在一起时,rollup 很神奇并创建了最有效的捆绑包。

这就是许多流行的图书馆正在做的事情。 实际上,RxJS 也将他们的import “逻辑”切换到了我描述的模式。 它看起来像:

 import { flatMap, map, tap } from 'rxjs/operators';

myObject.run().pipe(
  tap(result => doSomething()), 
  flatMap(() => doSomethingElse()), 
  map(() => doAnotherThing())
);

您可以在几篇博文中了解他们在 RxJS 6 中更改这些内容的“原因和方式”,例如: https ://auth0.com/blog/whats-new-in-rxjs-6/

但正如我所说,这只是一个想法,我不确定这会对代码库的其余部分产生什么影响。 当前的math模块也没有“准备好”像这样使用。 目前,数学模块上的所有方法都附加了“某种静态”。 这也可以防止汇总检测到真正需要的内容......

@roomle-build 嗯,所以您说汇总可以理解同一范围内的代码是否实际上不需要整个范围,很好。

您正在谈论转向功能性方法(函数获取对象)而不是面向对象的方法(具有成员函数的对象。)这是真实的,但鉴于 Three.JS 完全面向对象,提出这种类型的更改是相当大的一个,它会破坏所有现有的代码。

我不确定在这一点上支持这种改变的论点是否重要,以证明打破所有向后兼容性是合理的。

@makc不是真的。 这将是一个带有许多小“帮助”功能的大模块。 这也有助于摇树等。数学模块可能如下所示:

如果这是提议的内容,则应正确描述。 这是 Three.JS 从面向对象的设计风格到函数式设计风格的转变。

@roomle-build 嗯,所以您说汇总可以理解同一范围内的代码是否实际上不需要整个范围,很好。

是的,rollup 了解所有导入如何相互关联,并进行 tree-shaking、死代码消除等。新版本的 rollup 还可以进行“分块”和许多其他好东西。 但是当前的项目结构并没有充分利用这些特性。

您正在谈论转向功能性方法(函数获取对象)而不是面向对象的方法(具有成员函数的对象。)这是真实的,但鉴于 Three.JS 完全面向对象,提出这种类型的更改是相当大的一个,它会破坏所有现有的代码。

我不认为这两种范式是相互排斥的。 我认为你可以混合和匹配这两种范式。 我也不建议改用函数式编程。 我只是想描述一种摆脱循环依赖的方法。 您还可以将multiplyMatrices方法附加到Math对象。 但是如果有人重写这种东西,考虑使用 ES6 模块的特性是有意义的。 但正如我所说,我不是 Three.js 代码库的专家,我只是思考如何消除循环依赖。 我认为 Three.js 是一个很棒的项目,有很棒的代码库,我不想唠叨。 所以我希望没有人对我的评论感到冒犯😉

我不确定我们是否应该在一个问题中讨论设计决策。 你有什么地方更适合这种东西吗?

BTW gl-matrix 是一个函数式数学库: https ://github.com/toji/gl-matrix/tree/master/src/gl-matrix

@roomle-build

目前,数学模块上的所有方法都附加了“某种静态”。

怎么会这样?

@mrdoob我相信通过 gl-matrix 的功能设计,说 vec3 的每个功能(在我之前评论中链接到的 vec3 文件中)都是单独导出的。 这使您可以选择要导入的功能。 你不需要带上所有的 vec3。

与 Three.JS 一样,因为它使用面向对象的设计,Vector3 的所有数学函数都附加到 Vector3 对象原型,您只需导入 Vector3 类本身。

因此,Three.JS 中的导入是整个类,而使用函数式方法可以导入单个函数。

(关于 gl-matrix 库的另一个非常巧妙的事情是所有单独的函数不使用其他函数, @toji基本上已将所有数学的优化版本内联到每个单独的操作中。这在速度方面可能非常有效但这会导致库难以维护。)

我认为我们不需要重构 Three.JS 的这一部分,除非可能要摆脱 /math 中对 three.js 中其他目录的任何引用。 数学库相当小,这些天它从未真正出现在我的分析测试中。 是的,它不是最有效的,但它足够接近,同时保持可读性和易用性。

@bhouston知道了。 非常感谢您的解释! 😊

我只是想跟进这个话题。 但我想从importing function vs importing classes回到解决cyclic dependencies的话题。 (另外我不明白为什么import { someFunction } from 'SomeModule'的可维护性不如import SomeClass from 'SomeModule' ,但这绝对不是这个问题/对话的主题。

为了解决循环依赖,可以将功能放入单独的类中。 您可以将multiplyMatrices方法附加到 Math-Class 或创建具有multiplyMatrices方法的 Multiplier-Class。 但正如我之前所说,我不确定我们是否必须删除循环依赖项。 如果决定不删除它们,我认为这个问题可能已经接近了😃

解决 #19137 后,现在可以关闭了🎉。

@Mugen87哇,5 年了! 恭喜你终于成功了:fire::clap:
那时我在制作这些图表时玩得很开心 :smile_cat:

此页面是否有帮助?
0 / 5 - 0 等级