在我们所写的所有 cc.Component
之前都会有 @ccclass
这玩意,感觉是个不错的分享选题,来讲讲装饰器。
在 CocosCreator 中新建 ts 文件时,你有了下面这段代码:
const { ccclass, property } = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
}
而如果你新建的是 js 文件,则是这样的代码:
cc.Class({
extends: cc.Component,
properties: {
label: { default: null, type: cc.Label }
},
});
撇开干扰项 export default
,装饰器的感性认识还是挺直接的。
阔以认为它是一种语法糖,将原本的包裹关系,用 @
标识符简化了一丢丢写法,看上去更平面化了。
将其看做是返回一个函数的偏函数也行,更妙的是属性也能装饰。
同理,我们感性认识上就很容易想到装饰器的美妙用法了。
@readonly
someDataObject: {}
@eventLog('事件埋点')
handleClick(e) {}
@watcher('data.a.b')
onDataABChange(newValue, oldValue) {}
@priceWithDot
@priceWithUnit('$')
@toFixed(2)
price: '1000'
众所周知,使用 VScode 的 Ctrl + 左键
是有一定的代码溯源功能的,就能看到该属性或方法的 declare 声明或实际实现。
那么在 cc._decorator
上进行溯源,也就能看到它内部在当时设计时会有哪些东西了。
常用的 @ccclass
和 @property
就不提了。
在组件菜单中插入一项,方便快速引入。
表示必须带上某个组件,在引入时也会同时引入。
比如某个需要画图的就带上 cc.Graphics
,比如某个题目必会带上 NormalMessage
之类的。
一个节点不可带有两个相同组件,类似 cc.Sprite
不可多个那样的功能
通常是在运行时渲染,加上后可在 CocosCreator 中也渲染,如图所示。
但需注意,对于没有把握的代码,比如刚玩 gif 渲染做测试就不要开启,不然很容易造成编辑器卡死。
需与 @executeInEditMode
合用,当鼠标选中时渲染效果更优,暂时没遇到好场景。
渲染顺序,默认为 0
,越小越先,越大越后。除 onLoad
start
外,且每次 onEnable
都如此。
举个场景,比如先渲染子组件,然后父组件才开始使用它。
直接修改编辑器的属性检查器面板,需要写一些 Vue.component
注册的代码,path
指向该 js 的路径。
个人暂未进行试验,有兴趣的阔以按官网推荐进行试玩:
http://docs.cocos.com/creator/manual/zh/extension/extends-inspector.html
会多一个帮助文档按钮,@help('https://www.baidu.com')
你懂的
官方解释如下,基于上文的感性认知与案例后,想必也能够理解了。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。
装饰器使用 @expression 这种形式,expression 必须是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
在其他编程语言中也常有,因为确实是很方便的语法糖。
但需要区分的是,它是语言自带的还是阔以自建的,
比如 @override
就是 dart 语言系统自带的,但它不能创建 proposal-decorators。
而 typescript 只需在配置中进行开启,就阔以书写自定义的了。
拓展阅读:https://babel.docschina.org/blog/2018/09/17/decorators/
此外,装饰器是设计模式装饰模式中的一种广泛实践,它与外观模式和代理模式等是有所不同的,感兴趣的阔以再去拓展熟悉一下。
命令行 npm i -g typescript
后,新建 index.ts
、tsconfig.json
和 index.html
,
{
"compilerOptions": {
"experimentalDecorators": true
}
}
## 命令行运行将生产 ts 编译后的 index.js
tsc -w
<!-- 引入 index.js 后打开网页,即可看到 ts 中的打印啦 -->
<script src="index.js"></script>
function withLog<T extends { new(...args: any[]): {} }>(target?: T) {
return class extends target {
log = 'test log';
}
}
function defaultName(name: string) {
return function<T extends { new(...args: any[]): {} }>(target: T) {
return class extends target {
defaultName = name;
}
}
}
@defaultName('zyh') // 带参的装饰器
@withLog // 不带参的装饰器
class Demo {
name: string = '';
constructor(name: string) {
this.name = name;
}
}
console.log(new Demo('hello'));
只需最终 return
的也是个 class
就行,如果需要带参就多套一层。
回头再看一眼 @ccclass
有两种形态,他们的源码实现应该也就不难猜到了。
cc.Class({})
@ccclass class NewScript extends cc.Component {}
@ccclass('NewScript') class NewScript {}
function middleware(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const oldFunction = target[propertyKey]; // 获取方法引用
const newFunction = function (...args: any[]) {
console.log('call function ', propertyKey);
oldFunction.call(target, ...args);
}
descriptor.value = newFunction; // 替换原声明
}
class Demo {
@middleware
demo() { console.log('call demo'); }
}
const x = new Demo();
x.demo();
其中,target
为所在的类的实例,propertyKey
为装饰的方法名,descriptor
为对象描述信息。算是方法装饰器比较特别的吧。
此时,对象描述信息 又是一个可以去拓展学习的点了 ,其实也是就是对象是否可配置可枚举可改写的那些配置。
重新审视下上面的装饰器,你有发现什么问题吗?
是有问题的,假如我又写了个 middleware2
装饰在 demo
上,此时 target[propertyKey]
其实都是原函数,那么第二次修改的 descriptor.value
其实把第一次修改的相当于给覆盖掉了。
具体分析可自行写代码来理解:https://codepen.io/foreverZ133/pen/MWbGVqL
再来一个问题, @middleware @middleware2 demo() {}
时,哪个会先执行?答案是 @middleware2
。
function middleware(target: any, propertyKey: string) { }
语法相似,target
与 propertyKey
同上。
其实这玩意在我知道它只在初始化时才进行装饰时,有点希望破灭了的感觉,好像没卵用了呀。
当其实看多一些 React 项目就不难发现,mbox 的 @observable
等不就是属性装饰器嘛。
可能它更多的还是用在一些比较考验第三方能力的场景下,或者我还未能接触到的依赖注入等场景,思考不深。
class Demo {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
@timeCountDown countTime: 60;
}
function middleware(target: any, methodName: string, paramIndex: number) { }
语法稍有不同,methodName 为装饰的形参所在的函数名称,paramIndex 为形参所在位置。
实话实说,我尚未找到这类装饰器的有用场景。
- cc._decorator 中的装饰器已用到了项目中
- 阔以搭个 ts 环境写写自定义装饰器
- 欢迎分享更多装饰器的使用场景