🚀有时候,你有没有觉得在Cocos Creator里一遍又一遍地手动绑定节点是一件非常无聊的事?如果你同意,那么告诉你一个秘密:有一种方法!只需一行代码的"魔法",就可以自动绑定节点,省下频繁重复的手动拖拽节点的苦差事!好吧,这并不是真正的魔法,而是装饰器!🌈
装饰器是什么?
简而言之,装饰器在编程中就像是一个魔法药水。它们可以注入到类、属性、方法等中,提供某些额外的功能或修改它们的行为。而在TypeScript中,这种能力变得更加强大,因为装饰器可以和类型系统无缝结合。
进入正题:自动绑定节点的装饰器
🧙下面是一个名为bindComp
的装饰器函数。这个小家伙是我们今天的主角,它的使命是在onLoad
时,自动为所有注入的成员变量设置set&get方法,并在首次访问时为它赋值。它会让你在第一次访问这个属性的时候,自动在节点树上找到它并赋值给这个属性。嗯,就是这么酷!
typescript
// 查找指定路径下的节点或组件
function __find<T>(path: string, node: cc.Node, type: FIND_TYPE<T>) {
let temp = cc.find(path, node);
if (cc.js.isChildClassOf(type, cc.Component)) {
let comp = temp?.getComponent(type)
return comp;
}
return temp;
}
const _FIND_OPTIONS_ = "_FIND_OPTIONS_"
// 目标类型,通常用于查找组件
interface FIND_TYPE<T> {
new(): T;
}
// 查找选项的配置
interface FindOption<T> {
path: string; // 查找路径
type: FIND_TYPE<T>; // 目标类型
member: string; // 成员变量名称
root?: string; // 根节点的路径
}
/**
* @description 当onLoad时,自动对所有注入的成员变量设置set&get方法,当成员变量首次调用时对成员变量赋值
* @param path 相对于当前脚本this.node的搜索路径,当rootPath非空,则以rootPath为根节点查找
* @param type 查找组件类型
* @param rootPath 相对于this.node 的搜索路径,不传入时,以当的this.node为根节点进行查找
*/
export function bindComp<T extends cc.Component | cc.Node>(path: string, type: FIND_TYPE<T>, rootPath?: string) {
return function (target: any, member: string) {
if (!(target instanceof cc.Component)) {
Log.e("无法注入,仅支持 Component 组件")
return;
}
let obj: any = target;
if (!Reflect.has(target, _FIND_OPTIONS_)) {
// 重写onLoad,当onLoad时,自动对所有注入的成员变量设置set&get方法,当成员变量首次调用时对成员变量赋值
let __onLoad = obj.onLoad
obj.onLoad = function () {
let self = this;
let fOption = Reflect.get(self, _FIND_OPTIONS_)
for (let key in fOption) {
let ele: FindOption<T> = Reflect.get(fOption, key)
if (!Reflect.get(self, ele.member)) {
Reflect.defineProperty(self, ele.member, {
enumerable: true,
configurable: true,
get() {
let node = self.node;
if (ele.root) {
let rootMemberName = `__${ele.root.replace(/\//g, "_")}`
if (!cc.isValid(self[rootMemberName])) {
self[rootMemberName] = __find(ele.root, node, cc.Node);
}
node = self[rootMemberName];
if (CC_DEBUG && !cc.isValid(node)) {
Log.e(`${cc.js.getClassName(self)}.${ele.root}节点不存在!!!`)
}
}
if (!cc.isValid(self[key])) {
self[key] = __find(ele.path, node, ele.type);
}
return self[key];
},
set(v) {
self[key] = v;
}
})
}
}
__onLoad && Reflect.apply(__onLoad, this, arguments);
}
// 重写onDestroy,当onDestroy时,删除所有注入的成员变量
let __onDestroy = obj.onDestroy
obj.onDestroy = function () {
let self = this;
let fOption = Reflect.get(self, _FIND_OPTIONS_)
for (let key in fOption) {
let ele: FindOption<T> = Reflect.get(fOption, key)
Reflect.deleteProperty(self, ele.member);
}
__onDestroy && Reflect.apply(__onDestroy, this, arguments);
}
Reflect.defineProperty(target, _FIND_OPTIONS_, { value: {} })
}
// 将装饰器的配置信息存储到一个 `_FIND_OPTIONS_` 属性中,以便后续在组件的 `onLoad` 方法中使用
let option: FindOption<T> = { path: path, type: type, member: member, root: rootPath }
let attribute = `__${member}`
let fOption = Reflect.get(target, _FIND_OPTIONS_)
Reflect.defineProperty(fOption, attribute, { value: option, enumerable: true })
}
}
这段代码初看起来可能有些吓人,但其实它很简单,让我们逐步深入了解。
🤹一步一步解释代码吧!
1️⃣ 首先,我们的这个bindComp
装饰器接受几个参数:path
、type
、rootPath
。前两个必传,最后一个可传可不传。
path
是相对于当前脚本的this.node
的搜索路径。type
是要查找的组件类型。rootPath
,这个小家伙是个可选项,如果你给它传递了值,它会从这个路径开始作为根节点进行搜索
2️⃣ 在onLoad
方法中(也就是组件加载的时候),这个装饰器会走遍所有注入的成员变量,执行查找并绑定的任务。
3️⃣ 这个装饰器还会负责清理工作,在组件销毁的时候,它会在onDestroy
方法里删除之前绑定的属性,确保没有留下可能引发内存泄漏的垃圾。
🧐用起来是什么样子呢?
typescript
class MyComponent extends cc.Component {
@bindComp("path/to/node", cc.Node)
public myNode: cc.Node | null = null;
onLoad() {
console.log(this.myNode);
}
}
🚴就这样!就定义了一个myNode
,然后就可以直接用了,剩下的一切都不用担心!装饰器@bindComp
会负责查找和绑定。你所要做的只是在组件的代码里,愉快地使用这个myNode
就行了!
当myNode
首次被访问时,bindComp
会触发getter,这个getter会负责查找节点,并将它绑定到myNode
上。
🎉结束语
🚁装饰器真的是TypeScript的一大神器,它能让我们的代码更加干净、模块化。通过这种方法,我们不仅可以节省大量的时间,还可以减少出错的机会。
最后,请记住,任何技术手段都需要根据实际情况进行调整。虽然这种方法看起来很酷,但在实际项目中使用时,要确保它不会引入新的问题。