首页
/ three.js 资源加载核心:LoadingManager 加载管理器完全指南

three.js 资源加载核心:LoadingManager 加载管理器完全指南

2026-09-07 20:40:54作者:邓越浪Henry

导读

LoadingManager 是 three.js 中负责统计与跟踪所有"已加载/加载中"资源的统一管理器。无论你使用 OBJLoader、GLTFLoader 还是 TextureLoader,它们都默认挂在同一个全局加载管理器上;而当你需要为不同类别资源(如模型与纹理)分别展示独立的加载进度条、或者统一接管 URL 变换、文件类型分发与请求取消时,就需要创建并传入自定义的 LoadingManager。读完本文,你将掌握 LoadingManager 的全部回调、属性与 9 个核心方法的用法,并能结合源码理解其计数状态机与 loader 之间的协作机制。


一、LoadingManager 是什么

LoadingManager 的核心职责(实现源码 src/loaders/LoadingManager.js 的开头注释)是:

Handles and keeps track of loaded and pending data. — 处理并跟踪已加载与待加载的数据。

它本身不负责实际请求资源,真正执行 fetch/Image 等网络工作的是各个 Loader(如 FileLoader、ImageLoader),LoadingManager 更像一个调度与统计中枢:Loader 在开始、结束、失败时通知 manager,manager 据此驱动 onStart/onProgress/onLoad/onError 回调,并负责把 URL 交给"文件处理器"或"URL 变换函数"处理。

默认全局实例与何时需要自建实例

src/loaders/Loader.js 的构造函数中可以看到这一关键默认逻辑:

constructor( manager ) {
    this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager;
    ...
}

也就是说,任何 loader 在创建时若不传 manager,都会自动使用模块级单例 DefaultLoadingManager(定义于 src/loaders/LoadingManager.js)。例如:

new THREE.TextureLoader();        // 内部使用 DefaultLoadingManager
new THREE.FileLoader();           // 同上

对于大多数场景,全局默认实例已足够。文档明确指出一个典型例外场景:当你想为"几何模型"与"纹理"分别显示独立的加载进度条时,应为两组 loader 各创建一个独立的 manager,否则它们的进度会被混在同一个统计计数中。


二、基础用法:代码示例

把一个自定义 manager 分发给多个 loader,即可让它们共享同一套加载生命周期回调(原文档核心示例):

const manager = new THREE.LoadingManager();
manager.onLoad = () => console.log( 'Loading complete!' );

const loader1 = new OBJLoader( manager );
const loader2 = new ColladaLoader( manager );

运行效果:无论 loader1loader2 各自发起多少次资源请求,只有当两者全部加载完成时,manager.onLoad 才会被触发一次。这正是"合并统计、统一收口"的设计目的。

注意:manager 分发必须以"loader 创建时传入"的方式完成。像 OBJLoaderColladaLoader 这类 loader 位于 examples/jsm/loaders/ 目录(例如 examples/jsm/loaders/3MFLoader.js 所示,FileLoader 构造时接收并透传 this.manager),而它们内部的子资源加载器也会继承同一 manager,从而实现整棵加载树的统一跟踪。


三、构造函数与生命周期回调

3.1 构造签名

new THREE.LoadingManager( onLoad, onProgress, onError )

三个回调全部可选。构造函数实现见 src/loaders/LoadingManager.js

参数 类型 触发时机
onLoad Function 当所有待加载条目全部加载完成
onProgress Function 当单个条目加载完成
onError Function 当某个条目加载出错

构造函数内部还创建了 itemsLoaded = 0itemsTotal = 0 两个计数器和 isLoading = false 状态标志,它们构成了整个进度统计的"状态机"。

3.2 回调参数签名(源码级细节)

要写出真正可用的进度条,必须知道回调收到的参数。阅读 itemStart 实现itemEnd 实现,可以得到精确签名:

  • onStart( url, itemsLoaded, itemsTotal ) —— 由 itemStart 触发。注意:只有"第一项"开始加载时才触发,之后新增条目不再重复调用 onStart。源码中 if ( isLoading === false ) { ... onStart(...) } 正是这个语义。注释还特别说明:不把 onStart 放进构造函数赋值,是为了避免 issue #5689 讨论的时序问题。
  • onProgress( url, itemsLoaded, itemsTotal ) —— 由 itemEnd 触发。每完成一项调用一次,参数中的 itemsLoadeditemsTotal 可直接用于计算进度百分比 itemsLoaded / itemsTotal
  • onLoad() —— 无参数。在 itemsLoaded === itemsTotal 的瞬间触发(发生在最后一次 itemEnd 中)。
  • onError( url ) —— 由 itemError 触发,仅接收出错的 URL。

一个包含全部回调的进度条示例:

const manager = new THREE.LoadingManager();

manager.onStart = ( url, itemsLoaded, itemsTotal ) => {
    console.log( `开始加载:${url}(第 ${itemsLoaded}/${itemsTotal} 项)` );
};

manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
    const percent = itemsTotal === 0 ? 0 : Math.round( itemsLoaded / itemsTotal * 100 );
    progressBar.style.width = percent + '%';
};

manager.onLoad = () => {
    console.log( '所有资源加载完成!' );
};

manager.onError = ( url ) => {
    console.error( `资源加载失败:${url}` );
};

3.3 属性一览

属性 类型 说明
.onLoad Function | undefined 全部条目加载完成时执行,默认 undefined
.onProgress Function | undefined 单个条目加载完成时执行,默认 undefined
.onError Function | undefined 出错时执行,默认 undefined
.onStart Function | undefined 首个条目开始加载时执行,默认 undefined(仅当第一项启动时触发一次)
.abortController AbortController 用于取消使用该 manager 的 loader 正在进行的请求(懒创建 getter,见下)

其中 abortController 是定义在类上的 getter源码),首次访问时才会创建真实的 AbortController 实例并缓存在私有字段 _abortController 中。源码注释提到这一写法是为规避 Cloudflare workerd(issue #3657)的限制。


四、Loader 与 Manager 的协作机制:itemStart / itemEnd / itemError

4.1 三个方法的作用

  • .itemStart( url ):loader 开始加载一个条目时调用。会使 itemsTotal++,并在"首个条目"时触发 onStart
  • .itemEnd( url ):loader 加载完一个条目时调用。会使 itemsLoaded++,触发 onProgress;若此时所有条目都已结束,则触发 onLoad 并复位 isLoading
  • .itemError( url ):loader 加载出错时调用,触发 onError

文档将其定位为"任何使用该 manager 的 loader 在对应时刻都应调用"的通知接口。以底层文件加载器 src/loaders/FileLoader.js 为例,它在真实网络流程中的调用顺序为:

  1. 请求发出前调用 this.manager.itemStart( url )
  2. fetch 成功 → 触发各自的 onLoad 回调后,在 .finally() 中调用 this.manager.itemEnd( url )
  3. fetch 失败 → 触发 onError 后调用 this.manager.itemError( url ),随后依然由 .finally() 调用 itemEnd

由于 itemStart/itemEnd 一一配对,即便中途失败,计数器也能正确闭合,onLoad 不会因为个别失败而永远不触发。

4.2 多个 loader 共享计数的意义

因为 OBJLoader、ColladaLoader 乃至更底层的 FileLoaderImageLoaderImageBitmapLoader 都会透传同一个 manager,所以一次模型加载往往会在内部展开成"主文件 + 若干纹理 + 若干缓冲区"等多个条目,全部汇聚到 manager 的计数中——这正是它能驱动整页加载进度条的原因。

有几点需要留意:

  • 进度事件按"条目"而非"字节"统计,itemsLoaded 是完成条目数而非真实下载量;如需要精确到字节的进度,可参考 FileLoader 内部基于 ReadableStream 读取与 Content-Length 解析出的逐字节 ProgressEventsrc/loaders/FileLoader.js),它只传给 loader 自己的 onProgress 回调,不进入 manager。
  • TextureLoader 自 r84 起已不支持 per-texture 的 onProgress(见 src/loaders/TextureLoader.js 注释),但纹理仍会计入 manager 的条目统计中,manager.onProgress 不受影响。
  • 个别 loader 会做防御性处理:例如 AudioLoader 会在解码阶段再次调用 itemStart,以避免"加载管理器过早完成"(源码注释提及 issue #33378)。

五、文件类型分发:addHandler / removeHandler / getHandler

5.1 把特定扩展名路由给自定义 loader

.addHandler( regex, loader ) 注册一个"正则表达式 → loader"的映射,用于决定某类文件应该由哪个 loader 加载。典型场景是覆盖纹理的默认加载器。原文档示例:

// 为 .tga 纹理注册专用加载器
manager.addHandler( /\.tga$/i, new TGALoader() );

.removeHandler( regex ) 则按正则反注册:

manager.removeHandler( /\.tga$/i );

.getHandler( file ) 按文件路径查询当前命中的 loader;未命中任何规则时返回 null

三者均返回/引用 manager(removeHandlergetHandler 返回 null/loader 除外),因此支持链式调用。

5.2 源码实现要点

查看 handlers 的存储与查找实现

  • 内部用扁平数组交替存放 [regex0, loader0, regex1, loader1, ...]
  • removeHandler 通过 handlers.indexOf( regex ) 定位索引,找到后 splice( index, 2 ) 一次删除"正则 + loader"两项;
  • getHandler 按注册顺序线性遍历并执行 regex.test( file )返回第一个匹配的 loader;
  • 有一个易踩的坑:若传入的正则带有 g(global)标志,test() 会受 lastIndex 状态影响,因此源码在每次测试前强制 regex.lastIndex = 0(见 getHandler 实现,注释关联 issue #17920)。

5.3 实际消费方:GLTFLoader

addHandler/getHandler 是"生产者与消费者"配套设计。以 glTF 加载为例,examples/jsm/loaders/GLTFLoader.js 在处理图片资源时:

let loader = parser.textureLoader;
if ( source.uri ) {
    const handler = parser.options.manager.getHandler( source.uri );
    if ( handler !== null ) loader = handler;
}

即:先用 getHandler 查询管理器里有没有针对该 URI 注册过专用 loader,命中则改用该 loader 加载纹理。因此如果你给 manager 注册了 KTX2Loader 之类的压缩纹理处理器,加载含 .ktx2 图片的 glTF 时就能自动走压缩纹理通道,而无需改 GLTFLoader 自身的调用代码。


六、URL 改写:setURLModifier 与 resolveURL

6.1 何时需要 URL 改写

.setURLModifier( transform ) 允许在任何请求发出前,把每个资源 URL 交给回调 transform 处理一次。回调可以返回原 URL,也可以返回一个新 URL 来整体改写加载行为。文档列举的典型场景包括:从 .ZIP 压缩包内加载资源、对接拖放(drag-and-drop)API、以及使用 Data URI / Blob URL 等不能直接作为常规路径的资源来源。

6.2 配合 Blob URL 的官方示例

把 Blob 集合映射为 URL.createObjectURL() 生成的临时对象 URL,加载完成后统一释放,是文档给出的一段可以直接运行的完整代码:

const blobs = { 'fish.gltf': blob1, 'diffuse.png': blob2, 'normal.png': blob3 };
const manager = new THREE.LoadingManager();

// 用 URL 回调初始化 manager
const objectURLs = [];
manager.setURLModifier( ( url ) => {
    url = URL.createObjectURL( blobs[ url ] );
    objectURLs.push( url );
    return url;
} );

// 照常加载,随后释放这些 blob URL
const loader = new GLTFLoader( manager );
loader.load( 'fish.gltf', ( gltf ) => {
    scene.add( gltf.scene );
    objectURLs.forEach( ( url ) => URL.revokeObjectURL( url ) );
} );

要点拆解:

  • 回调入参是 loader 想要请求的"逻辑文件名"(如 'fish.gltf'),返回值才是真正会发出的请求地址;
  • URL.createObjectURL() 生成的临时地址必须在资源用完后通过 URL.revokeObjectURL() 手动释放,否则会持续占用内存,这是示例最后集中 revoke 的原因;
  • 同样的机制也适用于"拖入浏览器窗口的文件列表"场景:把 File 对象按文件名放入映射,即可让 glTF 及其引用纹理全部从拖放文件解析。

6.3 底层行为:resolveURL 与 NFC 归一化

.resolveURL( url ) 是 URL 改写落地的接口——每个 loader 在真正发起请求前都会先调用它。例如 FileLoader.loadObjectLoader 中均有 url = this.manager.resolveURL( url ) 的调用。

值得注意的源码细节:在 resolveURL 实现 中,URL 首先会被执行一次 url.normalize( 'NFC' )。注释说明这是为了将 Unicode URI(例如来自 glTF 的非 ASCII 文件名)按 RFC 3987 规范正确地做百分号编码。若设置了 urlModifier 则调用它,否则原样返回 URL。


七、请求取消:abort 与 abortController

7.1 方法签名与前提

manager.abort()   // 返回 manager 本身,便于链式调用

abort 实现 可以看到它做的事很简单:this.abortController.abort() 再清空内部控制器引用。真正的取消能力来自每个请求持有的 AbortSignal

文档给出两个生效前提,缺一不可:

  1. 具体 loader 实现了 Loader#abort()(基类方法默认空实现,见 Loader 的 abort 定义,返回 this 但什么都不做;可查阅文档页 Loader.html 了解各 loader 的覆盖情况);
  2. 浏览器支持 AbortSignal.any()

7.2 两层信号如何合并

为什么调用 manager.abort() 能取消"所有使用该 manager 的 loader"的请求?关键在于 FileLoader 构造请求时

const req = new Request( url, {
    ...
    signal: ( typeof AbortSignal.any === 'function' )
        ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] )
        : this._abortController.signal
} );

它把loader 私有的信号manager 共享的信号通过 AbortSignal.any() 合并成一个信号传给 fetch。于是两种取消入口都生效:

  • 调用单个 loader 的 loader.abort() → 只取消该 loader 的请求;
  • 调用 manager.abort() → 同时取消所有以该 manager 构造的 loader 的进行中请求。

同样模式也存在于 ImageBitmapLoader。注意 AbortSignal.any() 是较新的 Web 标准能力,在不支持的环境下代码自动退化为只使用 loader 自身信号(manager 级别的广播取消将不可用),这正是文档强调该前提的原因。


八、完整实战:一个带进度的多资源加载流程

综合以上知识,一个可运行的整体示例(把模型、纹理、字体统一纳入进度统计,并演示全部四个生命周期回调):

import { LoadingManager, GLTFLoader } from 'three';
import { TGALoader } from './examples/jsm/loaders/TGALoader.js';

const manager = new LoadingManager();

manager.onStart = ( url, loaded, total ) => {
    console.log( `开始加载第 ${loaded + 1}/${total} 项:${url}` );
};

manager.onProgress = ( url, loaded, total ) => {
    const percent = total ? ( loaded / total * 100 ).toFixed( 1 ) : 0;
    document.querySelector( '#bar' ).style.width = `${percent}%`;
    console.log( `进度:${percent}%(${loaded}/${total})` );
};

manager.onLoad = () => {
    console.log( '全部加载完成,可以开始渲染场景!' );
    // 例如:startAnimation();
};

manager.onError = ( url ) => {
    console.error( `加载失败:${url}` );
};

// 覆盖纹理默认加载器:.tga 交给 TGALoader
manager.addHandler( /\.tga$/i, new TGALoader() );

// 共享同一 manager
const modelLoader = new GLTFLoader( manager );
const texLoader = new THREE.TextureLoader( manager );

modelLoader.load( 'models/robot.glb', ( gltf ) => scene.add( gltf.scene ) );
texLoader.load( 'textures/robot_diffuse.tga' );
// 需要中断时:manager.abort();

当模型引用了多张贴图、多个 buffer 时,这些子资源同样会经由 resolveURLgetHandler 与计数通知流入该 manager,进度条因此能如实反映"从网络发出第一字节到最后一块 buffer 落地"的完整过程。


九、小结与易错点提示

围绕 LoadingManager 文档页 的主题,快速回顾关键结论:

  1. 计数机制:进度由"条目数"驱动,onStart 仅在第一项开始时触发一次;onProgress(url, loaded, total) 提供可计算的进度;onLoad 在所有 itemStart/itemEnd 配对完成后触发。
  2. 共享 vs 独立:不传 manager 时默认共享 DefaultLoadingManagersrc/loaders/LoadingManager.js);需要分类进度条或分类控制时就各自 new 一个。
  3. 文件分发addHandler + getHandler 配对使用,注意带 g 标志正则的 lastIndex 复位问题(源码已内部处理,自定义时尽量避免使用 global 标志);GLTFLoader 等加载器会按 URI 查询 handler 决定纹理加载器。
  4. URL 改写setURLModifier 作用于每次请求前,resolveURL 会先做 NFC 归一化再交由回调处理;从 Blob/拖放/.ZIP 取资源时必须记得 revokeObjectURL 释放。
  5. 取消加载manager.abort() 需要 loader 实现 abort() 且环境支持 AbortSignal.any(),否则仅能单 loader 取消。

理解这套"Loader 上报、Manager 统计、回调驱动"的协作契约后,无论是做全屏加载遮罩、分资源类型进度条、压缩纹理自动路由,还是拖放/内存资源加载与中途取消,都可以在 API 层轻松实现。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389