22.Three.js 优化之OffscreenCanvas 与WebWorker
22 Three.js 优化之OffscreenCanvas 与WebWorker
我们知道
关于
https://developer.mozilla.org/zh-cn/docs/web/api/web_workers_api/using_web_workers
或者查看我写的另外一篇文章:
一些比较新的浏览器
因此
OffscreenCanvas 必须搭配Web Worker 一起使用。
补充一个知识点:
和
OffscreenCanvas 类似的还有ArrayBuffer 、 MessagePort、ImageBitmap,他们都可以与WebWorker 搭配使用
OffscreenCanvas 的概念和用法
OffscreenCanvas 的基本概念
你可以把
而
注意,这里提到的 离屏渲染 和 我们使用
WebGLRendererTarget 来做的离屏渲染,从概念上是类似的
OffscreenCanvas 的 “离屏” 是指浏览器DOM 而言
WebGLRenderTarget 的 “离屏” 只指Three.js 的主场景(Scene) 而言
你可以把
但是请记得:目前绝大多数浏览器均已支持
如何创建
不可以使用
如何检测当前浏览器是否支持
我们只需检查
if(canvas.transferControlToOffscreen !== null){
console.log('当前浏览器支持 OffscreenCanvas')
}else{
console.log('当前浏览器不支持 OffscreenCanvas')
}
我们是通过检查
canvas 对象上是否包含.transferControlToOffscreen 方法来判断是否当前浏览器支持OffscreenCanvas 的。这里补充一个
JS 知识,如何判断某个对象上是否有某个属性。假设有一个对象
const obj = { a:undefined, b:null }
此时我们使用
if(obj.a) 或if(obj.b) 都是无法准确判断出到底 属性a 、b 是否存在。那么这个时候就可以使用以下
2 种方式来进行判断:
if('a' in obj) { ... } if(Reflect.has(obj,'b')){ ... }
使用
in 或者Reflect.has() 就可以准确判断出对象上是否具有某属性或方法,即使该属性的值为undefined 注意:上面
2 种查询方式都需要属性名的字符串值,为了更好的语法提示,我们示例中并不这样用。
OffscreenCanvas 的用法
再次强调:通常情况下
OffscreenCanvas 必须搭配Web Worker 一起使用。
我们单独创建一个
大体步骤:
-
创建一个单独的
JS 文件,用来编写Three.js 场景内容和渲染代码会以这个
JS 文件来作为web worker 的调用文件对象 -
在主场景中,获得
DOM 中的canvas -
获取
canvas 对应的OffscreenCanvas const offscreen = canvas.transferCountrolToOffscreen()
-
创建一个
worker 对象,并且设置一些消息参数const worker = new Window.Worker('xx/xxxx.js',{type:'module'}) worker.postMessage({type:'main',canvas:offscreen},[offscreen])
-
由于
web worker 不允许访问DOM 事件,例如浏览器窗口尺寸改变事件、鼠标事件等,所以当这些事件发生后,我们需要通知worker ,将事件对应的一些参数和变动发送给worker ,以便worker 中的canvas 渲染逻辑作出对应的响应。窗口尺寸改变事件我们还比较容易解决,无非就是把新的尺寸发送给
worker ,比较难的是像 鼠标事件、键盘事件等,需要稍微复杂的一些传递方式才可以解决。在本文后半部分会有详细讲解。
实际差异:
刚才将的是理论上大体步骤,但是由于我们本教程的示例代码,实际上是运行在
当然你也可以采取在编写
worker 时使用.js 而不是.ts ,只不过这样就失去了TypeScript 的便利性。
我们推荐的解决方案是使用
具体的配置步骤,请参考我写的另外一篇文章:
接下来,我们将通过一个实际的例子,来演示一遍
离屏画布渲染示例:HelloOffscreenCanvas
假设你已经配置好了
示例目标:
- 场景上有
3 个不同颜色,不断旋转的立方体 - 我们将场景中的渲染工作,从主程序中抽离出去,让
Web Worker 来负责场景的渲染工作,依次来减轻主程序的运算负担。
补充说明:
- 我们场景中动画渲染本身计算量并不是很大,即使我们不使用
web worker 浏览器也不会卡顿,但本示例只是为了演示如何使用OffscreenCanvas + Worker 。 - 我们先假设你的浏览器是支持
OffscreenCanvas 的。
关键点说明:
默认 主程序
而
因为本质上
index.tsx 和worker.ts 就不在同一个线程中,无法共享数据支持共享数据的
SharedWorker 目前浏览器支持度还不够高。
假设我们是把场景渲染的计算工作转移到了
Web Worker 本身是无法访问DOM 元素的
幸好
也就是说,
worker.ts 负责创建Three.js 场景和物体worket.ts 负责渲染得到 场景画面数据worket.ts 通过.postMessage() 将离屏渲染得到的 画布画面内容数据 发送给index.tsx index.tsx 接收 画布画面内容数据 并渲染到canvas DOM 中
而是走以下流程:
index.tsx 通过canvas.transferToOffscreenCanvas() 得到OffscreenCanvas index.tsx 通过.postMessage() 第2 个参数,将[OffscreenCanavs] 传递给worker.ts ,也就是说将canvas 的控制权完全交给worker.ts - 接下来就是
worker.ts 负责创建Three.js 场景和物体,并且渲染场景画面内容直接赋予给OffscreenCanavas 。
其他补充:
由于
所以当浏览器窗口尺寸发生变化后,我们要让
与窗口尺寸改变相似的还有鼠标移动事件,也可以通过传递当前鼠标坐标位置传递给
Worker 以便做出相应的处理。后期我们会学习如何做场景物体拾取效果,就是鼠标放到某个物体上时物体做出相应变化,这种场景就会需要用到鼠标坐标。
接下来,就开始具体编写代码吧。
message-data.ts
src/components/hello-offscreen-canvas/message-data.ts
注意:
message-data.ts 会同时被index.tsx 和worker.ts 引入,这样做的效果是:
index.tsx 可以比较容易知道worker.ts 内部定义的函数名叫什么worker.ts 可以比较容易知道index.tsx 传递过来的参数类型是什么
//定义画布的尺寸类型数据结构
export type CanvasSize = {
width: number,
height: number
}
export enum WorkerFunName {
main = 'main',
updateSize = 'updateSize'
}
//定义 MessageEvent data 的数据结构
export type MessageData =
{ type: WorkerFunName.main, params: OffscreenCanvas }
|
{ type: WorkerFunName.updateSize, params: CanvasSize }
worker.ts
src/components/hello-offscreen-canvas/worker.ts
import * as Three from 'three'
import { CanvasSize, MessageData, WorkerFunName } from './message-data'
let renderer: Three.WebGLRenderer
let camera: Three.PerspectiveCamera
let scene: Three.Scene
//定义初始化的函数
const main = (canvas: OffscreenCanvas) => {
//开始创建 3D 相关场景
renderer = new Three.WebGLRenderer({ canvas })
camera = new Three.PerspectiveCamera(45, 2, 0.1, 100)
camera.position.z = 4
scene = new Three.Scene()
const colors = ['blue', 'red', 'green']
const cubes: Three.Mesh[] = []
colors.forEach((color, index) => {
const material = new Three.MeshPhongMaterial({ color })
const geometry = new Three.BoxBufferGeometry(1, 1, 1)
const mesh = new Three.Mesh(geometry, material)
mesh.position.x = (index - 1) * 2
scene.add(mesh)
cubes.push(mesh)
})
const light = new Three.DirectionalLight(0xFFFFFF, 1)
light.position.set(-2, 2, 2)
scene.add(light)
const render = (time: number) => {
time *= 0.001
cubes.forEach((item) => {
item.rotation.set(time, time, 0)
})
renderer.render(scene, camera)
self.requestAnimationFrame(render)
}
self.requestAnimationFrame(render)
}
//定义用来接收画布尺寸更新的函数
const updateSize = (newSize: CanvasSize) => {
camera.aspect = newSize.width / newSize.height
camera.updateProjectionMatrix()
renderer.setSize(newSize.width, newSize.height, false)
}
const handleMessage = ((eve: MessageEvent<MessageData>) => {
switch (eve.data.type) {
case WorkerFunName.main:
main(eve.data.params)
break
case WorkerFunName.updateSize:
updateSize(eve.data.params)
break
default:
throw new Error(`no handle for the type`)
}
})
self.addEventListener('message', handleMessage)
const handleMessageError = () => {
throw new Error('Worker.ts: message error ...')
}
self.addEventListener('messageerror', handleMessageError)
//导出 {} 是因为 .ts 类型的文件必须有导出对象才可以被 TS 编译成模块,而不是全局对象
export { }
index.tsx
src/components/hello-offscreen-canvas/index.tsx
import { useEffect, useRef } from 'react'
import { WorkerFunName } from './message-data'
import Worker from 'worker-loader!./worker'
import './index.scss'
const HelloOffscreenCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useEffect(() => {
if (canvasRef.current === null) { return }
const canvas = canvasRef.current as HTMLCanvasElement
const offscreen = canvas.transferControlToOffscreen()
const worker = new Worker()
worker.postMessage({ type: WorkerFunName.main, params: offscreen}, [offscreen])
const handleMessageError = (error: MessageEvent<any>) => {
console.log(error)
}
const handleError = (error: ErrorEvent) => {
console.log(error)
}
worker.addEventListener('messageerror', handleMessageError)
worker.addEventListener('error', handleError)
const handleResize = () => {
worker.postMessage({
type: WorkerFunName.updateSize,
params: { width: canvas.clientWidth, height: canvas.clientHeight }
})
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
worker.removeEventListener('messageerror', handleMessageError)
worker.removeEventListener('error', handleError)
}
}, [canvasRef])
return (
<canvas ref={canvasRef} className='full-screen' />
)
}
export default HelloOffscreenCanvas
我们可以看到
运行调试一切正常。
接下来我们要解决
-
控制
3D 场景用到的OrbitControls 类,在新建时需要传递HTML DOM 元素,交互的过程中需要DOM 元素的鼠标事件和键盘事件,但是worker 内部又不能访问DOM 元素,那该如何解决? -
假设浏览器不支持
OffscreenCanvas ,那又该如何拆分我们的代码可以做到兼容?在软件开发术语中,会使用 “鲁棒性或健壮性” 来指代码的兼容性和容错性。
模拟并添加OrbitControls
你需要先忘记我们上面刚才讲过的示例代码,本小节中所有的代码和上面示例代码没有任何关联。
目前无法使用OrbitControls 的困境
在以前所有的例子中,我们添加镜头轨道控制器,都是使用以下代码:
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
const controls = new OrbitControls(camera,canvas)
或者
const controls = new OrbitControls(camera,window.body)
我们已经将
不要想着尝试将
DOM 元素作为 参数,使用.postMessage() 函数传递给Worker 内部,因为.postMessage() 函数中的参数并不是传递引用,而是直接深度复制一份。
那么究竟该怎么办呢?
我们先研究一下
https://github.com/mrdoob/three.js/blob/dev/examples/jsm/controls/OrbitControls.js
我把源码中和
//设置 scope = this
var scope = this;
var OrbitControls = function ( object, domElement ) {
//下面这行代码相当于 scope.domElement = domElement
this.domElement = domElement;
}
//添加鼠标右键(上下文)菜单事件侦听
scope.domElement.addEventListener( 'contextmenu', onContextMenu);
//添加触控笔和鼠标摁下事件侦听
scope.domElement.addEventListener( 'pointerdown', onPointerDown);
//添加鼠标滚轴事件侦听
scope.domElement.addEventListener( 'wheel', onMouseWheel );
//添加触摸开始、结束、移动事件侦听
scope.domElement.addEventListener( 'touchstart', onTouchStart);
scope.domElement.addEventListener( 'touchend', onTouchEnd);
scope.domElement.addEventListener( 'touchmove', onTouchMove);
//添加键盘事件侦听
scope.domElement.addEventListener( 'keydown', onKeyDown);
if ( state !== STATE.NONE ) {
//添加触控笔和鼠标移动事件侦听
scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove, false );
//添加触控笔和鼠标松开事件侦听
scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp, false );
scope.dispatchEvent( startEvent );
}
补充说明:在最新的浏览器事件中
pointer 相关事件即包含触控笔,也包含鼠标。所以pointerdown 、pointermove、pointerup 这3 个事件对应 触控笔或鼠标 对应的事件,相当于是2 者的合体。
从上面可以看出,我们初始化传递给
当然
OrbitControls 代码中也有对应的removeEventListener 移除事件侦听。
补充一点:
contextmenu 事件虽然目前部分浏览器支持( 主要是火狐浏览器) ,但是在MDN 的文档中已经明确该事件即将被废除。
重点来了,请听好:
-
既然
OrbitControls 需要DOM 元素的目的是为了获取并添加各种事件侦听 -
而
Worker 中无法获取DOM 元素 -
那么有没有可能我们虚拟出来一个对象,让该对象拥有和
DOM 元素相同的事件API 换句话说,就是让这个虚拟出来的对象具备抛出事件的能力
补充一点,这里说的
DOM 事件其实有2 种:DOM 元素上的各种用户交互5 种事件:鼠标事件、滚轴事件、键盘事件、触摸事件、右键菜单事件- 浏览器窗口尺寸发生变化引发
DOM 元素尺寸发生变化的window resize 事件 - 我们需要做的就是分别模拟出以上
6 种事件
-
然后我们让
OrbitControls 去侦听这个虚拟对象所发出的各种事件 -
也就是说让这个虚拟对象代替真实的
DOM 元素,以此来解决我们目前的困境
思路有了,那该具体怎么做呢?
首先我们要明白一件事,原生
而
究竟需要模拟出
此刻,我们只以事件携带的属性来举例说明:
-
对于鼠标滚轴滚动来说,
OrbitControls 只需要使用到该事件的deltaY 值 -
对于键盘事件来说,
OrbitControls 只需要使用到该事件的ctrlKey 、metaKey、shiftKey、keyCode 值meta 键?这个
meta 键在Windows 键盘上相当于windows 键、在苹果键盘上是一个四瓣的小花。在
OrbitControls 内部并未使用到altKey 值注意:由于
OrbitControls 仅使用到键盘 上/ 下/ 左/ 右4 个键,我们还可以主动过滤掉一些无用的摁键,换句话说也就是提前判断一下是否是以上4 个方向键,如果不是,则直接跳过,不传递该事件注意:对于目前版本
Three.js r126 版本而言,OrbitControls 键盘事件读取使用的是keyCode ,但是event.keyCode 事实上已经不被推荐使用,建议使用event.code 属性。所以我顺带向
Three.js 官方提交了PR ,将keyCode 修改为code ,这个PR 已经被官方审查通过了,会在r127 版本中使用。因此,我也成为Three.js 代码贡献者了。我提交的这个
PR :https://github.com/mrdoob/three.js/pull/21409关于为什么不再推荐使用
event.keyCode 主要是因为keyCode 不能够比较清晰正确返回键盘所摁键,例如 冒号和分号 都是同一个键,此时keyCode 就无法精确区分。 -
对于鼠标事件,
OrbitControls 需要使用到该事件的pointerType 、button、clientX、clientY、ctrlKey、metaKey、shiftKey 值 -
对于触摸
(touch) 事件,OrbitControls 需要使用每一个 触摸点的pageX 、pageY 属性值 -
…
我们需要让 “虚拟对象” 抛出 “虚拟事件”,并且虚拟事件拥有上面那些属性值。
补充说明:这里所说的 “抛出事件” 暗含
2 种事件:
用户交互事件:鼠标事件、滚轴事件、键盘事件…
浏览器窗口变化引发 “
DOM 元素” 内部尺寸属性相关的修改事件除了
window 拥有resize 事件之外,普通DOM 元素是不具备resize 事件的,我们可以将这个原本不存在的DOM 元素resize 追加到 我们的消息中,对于 用户交互事件 我们选择直接抛出、对于window resize 引发的尺寸修改事件,我们选择内部直接处理。
属性补充说明:
在
// scope = this
scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
因此我们模拟的元素还要拥有
在后面的实际代码中你会看到,我们会让 模拟元素
.ownerDocument 指向自身。this.ownerDocument = this
在
if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
也就是说在初始化
//@ts-ignore
self.document = {}
-
添加
//@ts-ignore 这样注释,可以让TypeScript 忽略下面一行代码的检查。因为
window.document 在TS 版本里被定义为 只读对象,是无法修改的。如果我们不选择忽略
TS 检查,当去执行self.document = {} 时TS 就会报错。 -
我们添加的
self.document = {} 纯粹是为了让OrbitControls 构造函数可以去访问到document 对象,避免报错。
所谓的 “抛出事件” 实现的途径是由 我们虚构出的一个 事件派发器 来实现的。
该事件派发器的
- 监听功能:监听真实
DOM 元素事件:用户交互事件、浏览器尺寸变化事件 - 事件转化:将监听到的事件内容,转化为一条数据,该数据包含该事件中
OrbitControls 所需要的各种属性值 - 消息传递:将事件转化后的数据,通过
web worker 的postMessage() 传递给web worker
所谓的“模拟的
该“模拟的
- 模拟
DOM 元素的属性和方法 - 处理
DOM 元素需要处理的各种事件
该“模拟的
-
index.tsx 中添加对 真实DOM 元素的监听,并且通知worker 创建“模拟元素”的消息 -
worker.ts 中接收消息,开始创建一个 “模拟元素”,并且把该元素传递给OrbitControls -
index.tsx 中监听到真实DOM 元素触发的各种事件,将该事件分析处理,转化为一条约定好的消息 并发送给worker -
worker.ts 中接收到事件消息后,将该消息转发给 “模拟的DOM 元素” -
“模拟的
DOM 元素” 接收该消息,然后将该消息( 包含事件类型、事件属性) 抛出,供OrbitControls 使用我们抛出的事件,也是模拟出来的事件,并不是原始
DOM 事件,只不过我们模拟出来的事件中恰好包含OrbitControls 需要的所有属性和方法。在
web worker 中工作的OrbitControls ,从始至终都不知道自己操作的其实是一个假的DOM 元素,以及监听的事件也是假的DOM 事件。
整个事件的流程为:
真实
通过 事件
我故意一直使用 “模拟的
DOM 元素”这个词,而没有使用 “虚拟DOM ”,就是为了让我们避免和react 中 虚拟DOM 的一词弄混淆。
关于 事件抛出 的补充说明:
我们让 “虚构元素” 继承于
import { EventDispatcher } from 'three'
为什么不使用 原生
这是因为原生
补充说明:
无论
浏览器中原生的各种事件处理函数 其实是异步的。
整体解决思路回顾:为什么我们可以实现?
回顾一下本小节开头的困境问题:
那么我们究竟是怎么做到的?以及为什么我们可以做到?
答:机缘巧合
目前火狐浏览器并不支持
OffscreenCanvas ,所以本示例还要考虑在非worker 情况下的场景创建。
再次提醒一下:在
web worker 中,不光DOM 元素是我们模拟出来的,就连抛出的DOM 元素事件也是我们模拟出来的。
最终我们让
如果你对我上面的讲述还不太理解,那么多读几遍,不要着急接着往下看。因为如果整体的思路你没有理解透,那么下面这些具体实现的代码,阅读起来也会一头雾水。
实话实说,在学习本章内容,看英文原版教程,我花了将近一周的时间,才弄明白整个原理。
本文讲的内容可能是整个
three.js 教程中最绕、最复杂的,但是为了性能优化,学习一下是非常值得的。
补充一个和本文无关的知识点:
除了
- 通过
css 修改DOM 元素尺寸 - 因为
window resize 事件而修改DOM 元素尺寸
如果你想监听某
https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
整体思路示意图

接下来开始讲解具体代码如何实现。
由于我使用的是
React + TypeScript ,加上我有一些自己代码理解和划分,所以我下面讲述的代码和原教程很多地方都不一样。
过程有点复杂,希望我能讲清楚。
代码模块规划
-
定义一个类
FictionalElement 继承于EventDispatcher ,用这个类的实例来模拟 “DOM 元素本身”请注意:
FictionalElement 只具备( 模仿、模拟) “DOM 元素” 本身的一些属性和方法( 例如width 、height、left、top、getBoundingClientRect() 等) ,但并不具备可以直接和web worker 通信的能力在模拟一些 “无用” 的方法时,例如
focus() 、preventDefault()、stopPropagation(),将该方法代码体内不添加任何代码,只是保证有这个方法但无需有实际执行内容。 -
定义一个类
FictionalElementManager 用来创建和管理所有FictionalElement 实例事实上我认为这一步骤不是必须的,因为实际项目中,绝大多数情况下网页中都只会有一个 用户交互元素
( 通常为canvas 或document.body) ,并不会有太多元素需要我们管理。但是本文对应的 原版教程 中有这一环节( 管理层) ,那么我们也继续遵循。为了区别不同的管理对象,我们将在内部给每一个
ElementProxyReceiver 添加一个对应的id 原版教程中,
id 是由FictionalElementManager 提供的,但是我认为这样并不合理,我采用的是通过外部传递id 为string 类型的值。 -
定义一个类
FictionalWindow 用来模拟window 请注意,目前来说
FictionalWindow 仅仅是继承了EventDispatcher ,并没有做其他设置,先这样做,也许未来有其他需求了,再根据需要扩展它。 -
定义一个类
ControlsProxy 用来负责基础的 真实DOM 元素与Web Worker 之间通信。当浏览器窗口尺寸发生变化时,我们模拟的 “
DOM 元素本身" 也应该会发生尺寸变化,而这个变化的触发并不是由FictionalElement 完成的,而是由ControlsProxy 来完成的。 -
定义一个类
OrbitControlsProxy 继承于ControlsProxy ,然后重写configEventListener() 和dispose() 你可能疑惑为什么我没有直接把代码写在同一个类里,而是要拆分成
2 个。我这样做的原因是想将一些基础的、共性的属性和方法抽离出来,这样未来有一天我们要去实现其他轨道控制器,都可以继承于ControlsProxy -
定义一个类
WorkerMessageType ,用来定义 发送消息 的类型每次都靠手写消息类型是不靠谱的,万一手抖拼写错字母了想检查出来都不容易。
-
此外,虽然本教程一直都是用的是
TypeScript ,但是在编写这些类的时候,考虑有些人并不使用TS ,所以我采用的是.js + JSDoc 的方式,通过JSDoc 代码注释的形式来定义不同消息的数据结构,没有使用.ts 。我也是因为这个示例而去学习了
JSDoc ,感兴趣可查看我另外一篇学习笔记:JSDoc 的安装与使用.md
以上仅仅是 “虚构” 的核心代码,除此之外,我们还需要编写对应的 “应用” 层面的代码:
-
index.tsx:
JS 主场景main 代码 -
worker.ts:
Worker 场景代码 -
create-world.ts:负责创建
Three.js 3D 场景的核心代码create-world.ts 中的代码并不知道自己将来是运行在Worker 中还是运行在 主场景中(Main)
如果你能坚持看到这里没有被我绕晕,那恭喜你。
是不是该展示具体代码了?
项目实际代码
原理都讲述完了,但是每个类的代码细节实在是太多,我也不想再细致讲述了,所以在
https://github.com/puxiao/using-orbitcontrols-in-worker
为了让全世界的人能看到我的这个代码,我竟然写了一个英文版
README.MD
你可以直接查看我写的简体中文介绍文档:
https://github.com/puxiao/using-orbitcontrols-in-worker/blob/main/README-zh_CN.md
说一下我的感受
本节后面的代码实现,花费了我有
- 从阅读原版教程,完全看不懂 理解不了
- 后来可以理解
- 改为自己的实现方式
- 不断得修改、优化代码
- 上传到
Github 编写文档
尽管掌握了本章所写的示例代码,但是这些对实际
但是!
因为不断的深入去理解,学习,我也有很多收获:
-
我成功向
Three.js 贡献了自己的一点点代码,尤其是在提交自己的PR 过程中,用蹩脚英语与Three.js 代码审查人员的不断沟通,是一次很神奇的体验。 -
通过
OrbitControls ,顺带我也看了其他 轨道控制器的 一些源码,事实上我原本的野心计划是编写出所有轨道控制器的ControlsProxy 版,但是时间和精力,这个事情只能暂时放下,等待将来或者其他感兴趣的人来编写吧。目前我的项目中只编写了
OrbitControlsProxy.js ,如果你感兴趣,也可以尝试编写出其他 轨道控制器对应的XxxControlsProxy 。提醒:如果你想编写其他轨道控制器的代理管理类,它需要继承于
ControlsProxy ,然后重写configEventListener() 和dispose() 这2 个方法。 -
学习了
JSDoc 注释规范,也就是即使我们不使用TypeScript ,通过JSDoc 注释依然可以进行类型定义。通过
JSDoc 的学习,让我对TypeScript 有更加深一层的理解。无论
TypeScript 还是JSDoc ,还是VSCode IDE 本身的代码提示和自动检查,本质上都仅仅在 开发阶段 对我们编写的代码进行约束,将来JS 运行阶段就管不了那么多了。 -
接触了
//@ts-ignore 这个特殊注释
本小节结束
本小节至此结束,同时也意味着本系列教程的 “优化” 篇章讲解完成。
接下来,我们要开始新的阶段学习,下一阶段才决定我们
下一阶段,我们要开始学习
加油,好玩有趣的事情终于要开始了。