通过装饰器将Angular的@Input和生命周期钩子等转化为流,利用Subject与Observable合并ngOnChanges和afterViewInit,从而高效实现响应式数据驱动模式。该方法消除了繁琐的get/set样板代码,支持组件复用,最终仅需一行装饰器就可以生成可观察的输入响应值流。
在 Angular 中,有个呼声一直很高:能不能把 @Input 和生命周期函数直接当成流来用?听起来有点抽象,其实实现起来并不复杂。比如下面这个简单的组件:
class NameComponent {
@Input() name: string;
}
目标是让它输出一个 "hello name" 的 @Output。如果能把 input 当成流处理,一切就顺理成章了。最直觉的做法是用 setter 触发 Subject:
长期稳定更新的攒劲资源: >>>点此立即查看<<<
class NameComponent {
private name$ = new Subject(1);
private _name: string;
@Input() set name(val: string) {
this.name$.next(val);
this._name = val;
}
get name() {
return this._name;
}
@Output() helloName = this.name$.pipe(
map(name => `hello ${name}`),
);
}
功能没问题,但你看出来了吧——太啰嗦了。每个 input 都要写一套 set/get,维护成本不低。
生命周期函数也有类似痛点。比如我们经常在 ngOnDestroy 里统一取消订阅,每次都手工写 Subject 和 next:
class NameComponent implements OnDestroy {
private destory$ = new Subject();
ngOnDestroy(): void {
destory$.next();
destory$.complete();
}
}
要是有多个生命周期要监听,重复劳动就更明显了。
回到 input 的问题。其实除了 setter,ngOnChanges 也能拿到 input 的变化。顺着这个思路,我们很容易想到两步走:
ngOnChanges 转成一个 stream,叫 onChanges$onChanges$ 里 map 出我们关心的那个 input 的值private onChanges$ = new Subject(); @Input() name: string; name$ = this.onChanges$.pipe( switchMap(simpleChanges => { if ('name' in simpleChanges) { return of(simpleChanges.name.currentValue); } return EMPTY; }), ) ngOnChanges(simpleChanges: SimpleChanges) { this.onChanges$.next(simpleChanges); }
这里有个细节:ngOnChanges 只在 input 变化时触发,所以组件初始化后的初始值需要额外处理。我们可以借助 afterViewInit 拿到首次值,再合并后续变化:
name$ = afterViewInit$.pipe(
take(1),
map(() => this.name),
switchMap(value => this.onChanges$.pipe(
startWith(value),
if ('name' in simpleChanges) {
return of(simpleChanges.name.currentValue);
}
return EMPTY;
)),
)
如果组件里有多个 input 需要转成流,上面这种写法的重复度还是很高。自然想到把它封装成一个通用方法:
export function getMappedPropsChangesWithLifeCycle( target: T, propName: P, onChanges$: Observable , afterViewInit$: Observable ) { if (!onChanges$) { return EMPTY; } if (!afterViewInit$) { return EMPTY; } return afterViewInit$.pipe( take(1), map(() => target?.[propName]), switchMap(value => target.onChanges$.pipe( startWith(value), if (propName in simpleChanges) { return of(simpleChanges.[propName].currentValue); } return EMPTY; )) ) }
更进一步,还可以把这个方法包装成装饰器。比如定义一个叫 InputMapper 的装饰器:
export function InputMapper(inputName: string) {
return function (target: object, propertyKey: string) {
const instancePropertyKey = Symbol(propertyKey);
Object.defineProperty(target, propertyKey, {
get: function () {
if (!this[instancePropertyKey]) {
this[instancePropertyKey] = getMappedPropsChangesWithLifeCycle(this, inputName, this['onChanges$']!, this['afterViewInit$']!);
}
return this[instancePropertyKey];
}
});
};
}
这里有个容易踩坑的点:target 是组件实例的原型对象,会被所有实例共享。所以不能用普通的属性赋值,而是要在 defineProperty 的 getter 里把变量绑定到 this 上,这样每个实例才能独立拥有自己的流。
使用起来就清爽多了:
class NameComponent {
private onChanges$ = new Subject();
private afterViewInit$ = new Subject();
@Input() name: string;
@InputMapper('name') name$!: Observable;
ngOnChanges() {
...
}
ngAfterViewInit() {
...
}
}
不过,onChanges$ 和 afterViewInit$ 的初始化代码还是得手动写。于是自然想到,能不能连生命周期函数也一起用装饰器搞定?
思路很简单:
subject.next()具体可以参考这篇文章的实现:Angular2+ Observable Life-cycle Events (induro.io)。这里直接给出装饰器:
export function LifeCycleStream(lifeCycleMethodName: LifeCycleMethodName) {
return (target: object, propertyKey: string) => {
const originalLifeCycleMethod = target.constructor.prototype[lifeCycleMethodName];
const instanceSubjectKey = Symbol(propertyKey);
Object.defineProperty(target, propertyKey, {
get: function () {
if (!this[instanceSubjectKey]) {
this[instanceSubjectKey] = new ReplaySubject(1);
}
return this[instanceSubjectKey].asObservable();
}
});
target.constructor.prototype[lifeCycleMethodName] = function () {
if (this[instanceSubjectKey]) {
this[instanceSubjectKey].next.call(this[instanceSubjectKey], arguments[0]);
}
if (originalLifeCycleMethod && typeof originalLifeCycleMethod === 'function') {
originalLifeCycleMethod.apply(this, arguments);
}
};
}
}
现在组件的代码可以简化成这样:
class NameComponent {
@LifeCycleStream('ngOnChanges') onChanges$: Observable;
@LifeCycleStream('ngAfterViewInit') ngAfterViewInit$: Observable;
@Input() name: string;
@InputMapper('name') name$!: Observable;
...
...
}
既然已经通过 InputMapper 依赖了 onChanges$ 和 afterViewInit$,那干脆在 InputMapper 内部自动生成它们,省得用户每次还要手动声明。我们把 LifeCycleStream 的核心逻辑抽成一个方法 applyLifeCycleObservable,然后在 InputMapper 里调用:
if (!('afterViewInit$' in target)) {
applyLifeCycleObservable('ngAfterViewInit', target, 'afterViewInit$');
}
if (!('onChanges$' in target)) {
applyLifeCycleObservable('ngOnChanges', target, 'onChanges$');
}
注意调用前要判断这个 stream 是否已经被定义过,避免重复覆盖。另外,这里不能直接写 target['ngAfterViewInit'],因为我们已经用 getter 劫持了属性,直接访问会触发 next 操作——想一想为什么?
最后,来看看最终的组件长什么样:
class NameComponent {
@Input() name: string;
@InputMapper('name') name$!: Observable;
}
一行装饰器,既保留了原生 @Input 的用法,又得到了一个由 ngOnChanges 和 afterViewInit 共同驱动的流。在实际项目中,这套方案能大幅减少样板代码,让数据流更贴近响应式编程的直觉。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述