开发者们,大家好。
我们计划在 3.8.4 版本中对引擎的 Tween 系统做一些强类型的改进。主要涉及如下几点:
1.将 ITweenOption
接口改为模版类 ITweenOption<T>
, 受影响的接口为 Tween.to/by。
3.8.4 之前
ITweenOption中定义的回调函数的 target 参数为
object`。
在开启 ts 严格模式
的情况下,用户实现的回调函数(比如:onUpdate)的 target 参数必须是 object
类型,否则会报如下错误:
tween(this.node)
.by(1, { scale: new Vec3(2, 2, 2) }, {
onUpdate: (target?: object, ratio?: number) => { // 只能定义为 object 才不报错
if (target) {
const node = target as Node;
// Do something with node
}
},
}).start();
上述代码中,如果将 target 类型改为 target? : Node
,则会报如下错误:
Type '(target?: Node, ratio?: number) => void' is not assignable to type '(target?: object | undefined, ratio?: number | undefined) => void'.
Types of parameters 'target' and 'target' are incompatible.
Type 'object | undefined' is not assignable to type 'Node | undefined'.
Type '{}' is missing the following properties from type 'Node': components, _persistNode, name, uuid, and 124 more.ts(2322)
用户只能通过as
将 object 强制转换为 Node 再进行下一步操作,这会导致业务代码比较冗余,as 的类型脱离了 ts 编译器的校验。
计划 3.8.4 开始
我们把 ITweenOption
改为 ITweenOption<T>
模版类型后,相当于 T 类型传递到 ITweenOption,onUpdate 函数的定义由
onUpdate?: (target?: object, ratio?: number) => void;
变为
onUpdate?: (target?: T, ratio?: number) => void;
这样用户代码变为:
tween(this.node)
.by(1, { scale: new Vec3(2, 2, 2) }, {
onUpdate: (target?: Node, ratio?: number) => { // 这里要求用户把 target 的类型定义为 Node,否则报错
if (target) {
// Do something with target
}
},
}).start();
2.Tween.show/hide/removeSelf/destroySelf 对 target 类型约束的改进
3.8.4 之前
只对这些接口的 API 注释中进行描述,要求用户如果有使用以上接口,target 必须传递 Node 类型,如果用户传递了其它非 Node 类型,这会在运行时
报如下错误:
Uncaught TypeError: this.target.getComponentsInChildren is not a function
at Hide.update (index.js:259626:42)
at Sequence.update (index.js:260862:23)
at Sequence.update (index.js:260849:29)
计划 3.8.4 开始
我们对 show/hide/removeSelf/destroySelf 这几个接口的定义做了调整,由原来的
hide (): Tween<T> { ... }
改为
type TweenWithNodeTargetOrUnknown<T> = T extends Node ? Tween<T> : unknown;
hide (): TweenWithNodeTargetOrUnknown<T> { ... }
这样在编码阶段,就能够知道传递了错误的类型,并报如下错误:
Object is of type 'unknown'.ts(2571)
(method) Tween<MyType>.hide(): unknown
3. Tween.call 对 callback 函数的参数类型进行约束,并添加自定义数据参数
3.8.4 之前
Tween.call 的 callback 类型定义为 Function
:
// eslint-disable-next-line @typescript-eslint/ban-types 这里还跳过检查。。。
call (callback: Function): Tween<T> { ... }
Tween.call 内部其实是调用 CallFunc 实现的,其实 CallFunc 的构造函数接收 callback, callbackThis 和 data 三个字段。
export class CallFunc extends ActionInstant {
constructor (callback?: Function, callbackThis?: any, data?: any) { ... }
}
Tween.call 的内部实现只传递了大而全的 Function
类型的 callback 参数,即没有对类型做任何的约束。
另外由于 Tween.call 只传递了 callback 参数,传递给 callbackThis 和 data 的字段其实是可选的,即 undefined。
这样会导致几个问题:
- 回调函数没有被约束,可以定义任何样子,任何多个参数,比如 tween.call((a, b, c, d)=>{…})
- 上层没法调整回调函数中的 this。当然,可以通过
fn.bind(thisObj)
的方式改,但是多了一个 bind 过程也多一个小开销。 - 上层没法透传一些自定义数据给回调函数
计划 3.8.4 开始
针对如上问题,我们修改了 call 的定义,对 callback 类型进行了约束,如下:
export type TCallFuncCallback<TTarget, TData> = (target?: TTarget, data?: TData) => void;
export class Tween<T> {
...
call<TCallbackThis, TData> (
callback: TCallFuncCallback<T, TData>,
callbackThis?: TCallbackThis,
data?: TData): Tween<T> { ... }
}
用户定义的 call 的回调函数,必须是符合此签名的函数类型。例如, target 的类型为 Node,data 的类型是 number,那么回调函数指定定义为:
// 1. 完全无参的回调,实际上回调函数还是有两个参数,分别是 target 和 data,其中 data 为 null
tween(this.node).call( () => {
// Do something
});
// 2. 带参数的回调,但没传 target 和 data 参数
tween(this.node).call( (target?: Node, data?: number) => {
// Do something
});
// 3. 带参数的回调,并且传递了 target 和 data 参数
class MyFoo extends Component {
foo (target?: Node, data?: number): void {
// 这里 data 为透传的参数 123
}
doTest (): void {
// 可以把 callback 定义为类的成员函数,并且把 this 传递进去保证回调 foo 函数的时候 this 的正确,123 为自定义参数
// 支持使用这样的用法,可以避免使用闭包函数中间饶一次转
tween(this.node).call(this.myfoo, this, 123).start();
}
}
重点来了:兼容性讨论 (已不存在,感谢 @SmallMain 提供方案)
铺垫了这么多,新类型更安全了,但是的确也一定程度破坏了兼容性,因此出这个帖跟大家交流一下。
针对第一点,onUpdate 那个改动 target 的改动,我觉得问题不大,因为用户如果继续用 object 也不会破坏兼容。
针对第二点和第三点,之前的如下这种隐式类型就会报错:
// 示例代码
onLoad () {
this.tweenParallel = tween(this.node)
.parallel(
tween().to(2, { scale: new Vec3(1, 2, 3) }), // 这行会报错,因为 tween() 为 Tween<unknow>, parallel 的签名要求是:(method) Tween<Node>.parallel(...args: Tween<Node>[]): Tween<Node>,不能把 unknown 类型赋值给 Node
)
}
Argument of type 'Tween<unknown>' is not assignable to parameter of type 'Tween<Node>'.
The types returned by 'hide()' are incompatible between these types.
Type 'unknown' is not assignable to type 'Tween<Node>'.ts(2345)
之前没问题,我测试了一下 Tween<unknown>
类型赋值给 Tween<Node>
不会报错:
let t1: Tween<unknown> = tween();
let t2: Tween<Node> = tween(new Node());
t2 = t1; // 3.8.4 之前不报错
3.8.4 开始会报错:
Type 'Tween<unknown>' is not assignable to type 'Tween<Node>'.ts(2322)
let t2: Tween<Node>
个人觉得改进后的更符合强类型的预期。
上述出问题的示例代码的改造方法其实也很简单:
onLoad () {
this.tweenParallel = tween(this.node)
.parallel(
tween<Node>().to(2, { scale: new Vec3(1, 2, 3) }) // 尽管我们没传 target,这里显式约定 target 的类型是 Node,这样也能约束 { scale: new Vec3(...) } 是符合传递给 Node,比如 scale 如果被错误写为 scale1,那么会报错,这样也能一定程度上避免一些低级错误。
)
}
因为改进,我们的确破坏了兼容性。之前一些隐式的 tween() 或者 tween({}) 要改为 tween<你的类型>()
,因此用户的升级改造成本应该还是相对较低的。
借助此帖,我们倾听广大开发者的建议和意见,如果有更好的解决方案,大家多多交流。谢谢。