自定义 @装饰器 装饰的属性没有被初始化

对于应用了重定义 get/set 的 @装饰器 的属性, babel 无法在定义该属性的时候 初始化 它的值, 而用 tsc 编译后正常.

希望能修复这个问题, 或者允许用 tsc 取代 babel

  • Creator 版本: 3.4.1

  • 重现方式:

const logged = (target, prop, desc?):any =>
{
	const key = `_$_${prop}`
	return {
		get() { console.log(`@logged: get ${prop} (${this[key]})`); return this[key] },
		set(v) { console.log(`@logged: set ${prop} to ${v}`); this[key] = v},
	}
};

class Hello
{
	@logged x = 123;
}

let h = new Hello();
console.log("x=" + h.x);

结果:

$ npx tsc index.ts --outFile index_tsc.js --experimentalDecorators --target es6 && node index_tsc.js
@logged: set x to 123  # tsc 正常初始化了 x
@logged: get x (123)
x=123 

# ["@babel/plugin-proposal-decorators", {"version": "legacy"}],
$ npx babel --extensions .ts index.ts -o index.js && node index.js
@logged: get x (undefined)
x=undefined # babel 没有执行初始化语句 x = 123 

试一下这样:

const logged = (target, prop, desc) => {
	const key = `_$_${prop}`;

    // 加上这一句,初始化那个隐藏属性
    if (desc.initializer) {
        target[key] = desc.initializer();
    }

	return {
		get() { console.log(`@logged: get ${prop} (${this[key]})`); return this[key] },
		set(v) { console.log(`@logged: set ${prop} to ${v}`); this[key] = v},
	}
};

class Hello {
	@logged x = 123;
}

let h = new Hello();
let i = new Hello();

console.log(`h.x=${h.x}, i.x=${i.x}`);

h.x = 456;
console.log(`h.x=${h.x}, i.x=${i.x}`);

我这边输出:

@logged: get x (123)
@logged: get x (123)
h.x=123, i.x=123
@logged: set x to 456
@logged: get x (456)
@logged: get x (123)
h.x=456, i.x=123
1赞

感谢回复,
这个方案有 2 个问题:

  1. 因为装饰器并 不是在每个对象实例 上执行 (target 参数在这里大约是 proto) , 这就导致 initializer 无法访问 this, 只能返回常量, 并且所有实例共享了这个初始值 (不应该).
class Hello
{
	_x = 123;
    // 在 @logged 中调用 initializer() 时无法得到 this, 所以依然 undefined
	@logged x = this._x; 
}
  1. 初始化的时候, setter 没有被调用. (当然可以在装饰器里手动调用, 但由于 问题1, 实际上也没有意义)

这两个问题有一个 workaround:
就是在 getter 里第一次执行时调用 initilizer.call(this). 但是这样就只会在第一次 get 的时候执行, 如果 不访问 x 就永远不会执行初始化逻辑 (可能包含 side effect), 所以也不是有效方案.

实际上我还尝试过比如在装饰器里 Object.defineProperty, 但我观察了 babel 输出, 它会自己用 defineProperty 覆盖装饰器中的定义…死局.

只能期待装饰器的提案了

如果你愿意在每个需要这个功能的类前面加个装饰器,我这里有个办法:

const initializeTag = Symbol('[[Initialize some shadowed fields]]');

const logged = (target, prop, desc) => {
    // As class decorator
    if (typeof prop === 'undefined') {
        return class extends target {
            constructor(...args) {
                super(...args);
                this[initializeTag]?.();
            }
        }
    }

	const key = `_$_${prop}`;

    const injected = target[initializeTag];
    target[initializeTag] = function (...args) {
        injected?.call(this, ...args);
        this[key] = desc.initializer.call(this);
    };

	return {
        get() { console.log(`@logged: get ${prop} (${this[key]})`); return this[key] },

	    set(v) { console.log(`@logged: set ${prop} to ${v}`); this[key] = v},
	}
};

使用:

@logged
class Hello {
	@logged x = 123;
}

这能够想到, 但是太不优雅. 况且我有大量 codebase 需要与其它项目共用.

我目前的解决方案: src =tsc=> assets/src, 这能解决问题, 虽然更慢了.

但我真正想说的是: 希望 cocos creator 能允许开发者切换 tsc / babel

抱歉,暂时没有计划,我们首先其实没有暴露我们用的什么编译器,这使我们以后切换编译器后端到 swcesbuild 等更现代的工具变得可能。

其次,装饰器这里是个特例,babel 和 tsc 的装饰器模型相差比较大,这也是因为装饰器提案久久不能落地。所以我们把装饰器视为一个不稳定的功能。

再者,我们需要更细粒度地控制代码的编译,babelswcesbuild 等可以通过 browserslist 控制代码编译,而 tsc 只能通过版本控制。

不过,我们可能换一种方式让用户可以定制脚本编译,接近于你目前的 src =tsc=> assets/src,就是在我们编译脚本之前执行用户的 HOOK,但这个接口也不会暴露我们的内部实现,只提供 代码文本 -> 代码文本 的接口,类似于以下形式:

function transform(code: string, id: string): string;
2赞

了解了.

最后, 其实 cc 用 tsc 作为前端输出 ES 再转给 babel/swc/esbuild 进行 pollyfill 好像也没啥毛病 ( cc 还可以利用 tsc 的错误信息优化 尴尬的 MissingScript 提示之类吧…)

你好,我想请教下这种使用属性装饰器重定义存取器的相关点在哪里可以找到,我想看看了解下谢谢。目前我只能找到在ts中使用属性装饰器的文章和例子,这种比较常见的属性装饰器定义方式。

你好,我想请教下这种使用属性装饰器重定义存取器的相关点在哪里可以找到,我想看看了解下谢谢。我在官方文档中没有搜索到相关内容,请问这方面的内容在哪里能够获取到呢?

https://es6.ruanyifeng.com/#docs/decorator

感谢,比大部分网上的专题要详细许多。

虽然帖子比较久了,但还是想问下这个编译脚本之前 执行用户HOOK的机制有了吗?