我就不该大半夜点开引擎的 issues,一点开就气得花半小时发贴。
事情的起因
是这个 issue:https://github.com/cocos/cocos-engine/issues/16937
在用引擎的 tween 缓动系统对节点做动画时,以下代码会报错:
tween(node).to(
1,
{ ... },
{
// ts(2322) error here
onUpdate(target: Node) { ... },
},
);
大家如果直接看这段代码,可能看不出啥问题,target
确实是一个节点,但 ts 却会报错。
是的,这个报错确实如 finscn 所说,是引擎 dts 的问题。
来看引擎的类型声明:
onUpdate?: (target?: object, ratio?: number) => void;
target
被声明为了 object
,其实最正确的声明 finscn 也说出来了,那就是将其声明为泛型 T
:
onUpdate?: (target?: T, ratio?: number) => void;
这样,ts 会根据你在 tween(node)
传入的类型,来自动推断出 target 是什么类型。
引擎组的态度
说到这,即使不深究,问题也已经清楚了,但为什么这个 issue 还有高达 24 条评论呢?
(如果继续阅读,建议先看看原 issue 的内容)
一个是因为 finscn 的 issue 是要求将 target
从 object
类型改为 any
类型。
这显然不合理,因为使用 any
,确实可以解决问题,但我们就没有必要去使用 ts 了,所以引擎组的应该也是这么认为的,所以发生了很多争论。
我个人猜测其实 finscn 是从一开始就知道应该改为泛型,而不是 any
,之所以 issue 这么写,应该就是觉得改成泛型可能对引擎组比较麻烦,引擎组就又会一直拖,而改成 any
其实很快,秉承着又不是不能用的理念,就妥协了。
另一个,并且我认为是最重要的原因就是,
引擎开发组宁愿手打 50 行代码,
花几天的时间搁着掰扯来掰扯去想要证明现在的 dts 没有问题,
都不愿意自己亲手试试输入一下用户提出的那段在不格式化时只有几行的会报错的代码。
就是因为和引擎组反映问题有时候会如此费劲,所以我也深有同感。
争论的答案
其实直到我发帖时,最后的几条评论是:
也就是最后依然没有搞清楚为什么上面那段代码会报错的具体原因。
为什么这样声明时:
onUpdate?: (target?: object, ratio?: number) => void;
我们将这个函数:
onUpdate(target?: Node, ratio?: number) { ... }
传进去时却会报类型不兼容的问题呢?明明 Node
也是 object
,他们应该是兼容的啊:
// 不会有任何问题
let obj: object = node;
评论里提到的严格模式其实并不是根本问题,而引擎组竟还提了嘴 eslint 真是把我气笑了。
这里的根本问题是类型的协逆变:https://zh.wikipedia.org/zh-cn/协变与逆变
简单来说以下代码能工作:
// obj 变量只能赋值为一个能兼容 object 类型的值,而 Node 确实兼容。
let obj: object = node;
而以下代码不能工作:
// fn 变量需要一个能处理 object 类型参数的函数,而赋值给它的函数只能够处理 Node 类型参数。
let fn: (obj: object) => void = (node: Node) => {};
如果还是有点想不通,那么先假设上面的代码能通过类型检查,以下这段代码就会出现问题:
fn(component);
你赋值给 fn 的函数以为传入的一定是 Node,但其实可以是其他 object 的字类型,比如是 Component,而这就会导致代码的运行出现错误。
这和是否严格模式没有太大关系,甚至和 ts 都没有什么关系,这是 c#、c++ 等任何编程语言都通用的知识。
普通的开发者不太了解,其实也正常,但引擎的开发者不了解,不好意思,我不太理解。
那么最后还有个问题,既然和严格模式没关系,为什么关闭严格模式会让错误消失?
因为这个选项:strictFunctionTypes
直接引用 ts 官方文档的话:
During development of this feature, we discovered a large number of inherently unsafe class hierarchies, including some in the DOM. Because of this, the setting only applies to functions written in function syntax, not to those in method syntax:
意思大概就是需要兼容 js 有太多类型协逆变不安全的代码,所以只有当开启这个选项,才会严格检查参数和返回值的类型。
不开启时由于不会检查所以也就不会报错了。
而无论是否开启,ts 中,类方法永远也不会进行严格检查,因为 ts 还没有显式标记协逆变的方法。
说到这,其实说他们对引擎自己的脚本系统不了解,都算轻的了。
这只是关于类型的通用编程知识。
这只是引擎非常常用的模块。
他们表现出来的是甚至都没有怎么使用过。
而且我认为他们表现得非常傲慢。
请指出我上述的测试代码,为什么没有你说的报错
自己不去弄清楚原因,让普通用户指出为什么,why?
就不说引擎的其它更多的问题了,比如这个:
const data: Prefab | Node;
const node = instantiate(data);
// ts(2339) error here
node.getComponent(Label);
以上代码,node
会被推断为 Prefab | Node
而导致 ts 报错,但这个用法是正确并且并不算偏僻的,但是因为错误的引擎 d.ts 声明而报错。
以上种种的问题如果你懂的时候,确实你自己 as
一下就解决了。
然后这些问题就不会有人提出来,而是默默踩坑。
但当开发团队进来新人的时候呢?
刚学习 cocos 的学生们呢?
他们甚至可能还没法说清楚 js/ts 的关系,他们只能不断地重复地踩坑。
他们会觉得非常匪夷所思,why?我只想实例化一个节点出来而已,为什么就会产生报错?
你指望他们去了解这么多吗?刚学习的第一天就告诉他们什么是协逆变?告诉他们如何绕过引擎 dts 问题?
他们只会选择另一个引擎,然后说这个引擎写个 xx 都有问题!
引擎组傲慢、不重视的问题也不是一天两天了:
https://github.com/cocos/cocos-docs/issues/2733
误人子弟的文档到现在也不改。
没有新人哪有未来?