当前位置: 首页
前端开发
Angular实践:将Input与Lifecycle转换为流示例详解

Angular实践:将Input与Lifecycle转换为流示例详解

热心网友 时间:2026-06-16
转载

通过装饰器将Angular的@Input和生命周期钩子等转化为流,利用Subject与Observable合并ngOnChanges和afterViewInit,从而高效实现响应式数据驱动模式。该方法消除了繁琐的get set样板代码,支持组件复用,最终仅需一行装饰器就可以生成可观察的输入响应值流。

将 Input 和生命周期函数转换成流

在 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(通过 Symbol 键避免冲突)
  • 保留组件原有的生命周期方法
  • 通过原型重写那个方法,在原始逻辑执行前先调用 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 的用法,又得到了一个由 ngOnChangesafterViewInit 共同驱动的流。在实际项目中,这套方案能大幅减少样板代码,让数据流更贴近响应式编程的直觉。

来源:https://www.jb51.net/article/275663.htm

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
如何用拖放在Flask看板应用中更新数据库状态

如何用拖放在Flask看板应用中更新数据库状态

在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。

时间:2026-07-09 07:01
如何用CSS :has()选择器快速实现悬停span显示相邻div

如何用CSS :has()选择器快速实现悬停span显示相邻div

利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。

时间:2026-07-09 07:01
display:block居中段落文本的响应式实现方法

display:block居中段落文本的响应式实现方法

通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。

时间:2026-07-09 07:01
JavaScript代码去重:高效避免重复编写的技巧

JavaScript代码去重:高效避免重复编写的技巧

将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。

时间:2026-07-09 07:00
使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。

时间:2026-07-09 07:00
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜