我们在工作中建立了一个图书馆,名为ngx-observable-lifecycle.
它基本上挂接到Angular 组件(ngOnInit、ngOnChanges等)的所有本机钩子,然后给出一个对象,其中每个键都是给定钩子的名称,值是调用该钩子的时间的可观察值.
可以把它看作一个小的包装器实用程序,它能够以一种更具react 性的方式工作,而不必手动创建Subject
并在挂钩发生时进行推送.
这一切都运行得很好,except在一个非常、非常、非常具体的 case 中:ngOnChanges
.
我们构建了一个简单的演示,其中父组件定义了一个计数器,单击按钮使其递增,然后将该计数器传递给子组件的输入.这里没什么花哨的.
然后,我们使用库从子组件中挂钩ngOnChanges
并订阅可观察对象.
- 启动应用程序时,会设置初始计数器值,订阅者会通知我们收到了预期的计数器值
- 当我们递增时,
ngOnChanges
不会发生,这是这里的主要问题.我能看到doCheck
、afterContentChecked
、afterViewChecked
,但看不到ngOnChanges
- 从那时起,我们可以想增加多少次就增加多少次,它会按预期工作
我不能把我的头绕着这一点,为什么第一个工作,而不是第二个,和所有其他的工作后罚款?据我所知,图书馆里没有skip
或类似的东西.
图书馆在13号角,但光线很亮.我试着升级到Angular 15,同样的交易.我已经创建了一个运行最新版本(17.1.2)的Stackblitz repo,同样的东西.
所以好消息是,它很容易复制.但我不明白根本原因是什么.
你可以找到reproduction I've made in this Stackblitz美元.
- 打开浏览器控制台(不是Stackblitz一个),有更多的东西显示出于某种原因
- 请注意,当应用程序启动时,您将看到第
{changes: {…}}
行,这是我们要进行的调试.它是由我们的ngOnChanges
个观察点触发的日志(log) - 点击"Increment InputValue",注意这次你不会记录
{changes: {…}}
- 再次点击增量按钮,从现在开始,它将一直工作.BUT,如果你看一下记录的更改,它是最后一个更改检测周期的更改,它总是晚一个值
Stackblitz中的代码 struct 很简单:
- 一个主组件,其将递增的值作为输入提供给子组件
- 获取作为输入的值的一个子组件
- 一个包含库的全部代码的文件(它是一个很小的库,总共150行,因此所有内容都在这个文件中)
我已经排除了classic 的问题,即输入传递具有相同引用的对象,而不是传递新的对象引用,因为我们在这里只是传递一个增量输入.从视图中,您将能够知道当前值是什么(并且它在视图中是正确的值),但是在日志(log)中,您将看到在损坏的部分之后,它总是晚了一步.
哦,还有one more really important detail个更让我困惑的问题.在子组件中,如果取消对正常的ngOnChanges
钩子(为空)的注释,则一切都会按预期进行.
我不知道这怎么会有影响,因为我们对原始挂钩的唯一引用是这样的:
const originalHook = proto[hook];
proto[hook] = function (...args: any[]) {
originalHook?.call(this, ...args);
对我来说,它看起来相当安全.我们保存原始钩子,用我们自己的函数覆盖它,该函数首先调用现有的钩子,如果有原始钩子,则在应用自定义逻辑之前.
代码的主要部分如下:
function getSubjectForHook(
componentInstance: PatchedComponentInstance,
hook: LifecycleHookKey
): Subject<void> {
if (!componentInstance[hookSubject]) {
componentInstance[hookSubject] = {};
}
if (!componentInstance[hookSubject][hook]) {
componentInstance[hookSubject][hook] = new Subject<void>();
}
const proto = componentInstance.constructor.prototype;
if (!proto[hooksPatched]) {
proto[hooksPatched] = {};
}
if (!proto[hooksPatched][hook]) {
const originalHook = proto[hook];
proto[hook] = function (...args: any[]) {
originalHook?.call(this, ...args);
if (hook === 'ngOnChanges') {
this[hookSubject]?.[hook]?.next(args[0]);
} else {
this[hookSubject]?.[hook]?.next();
}
};
const originalOnDestroy = proto.ngOnDestroy;
proto.ngOnDestroy = function (this: PatchedComponentInstance<typeof hook>) {
originalOnDestroy?.call(this);
this[hookSubject]?.[hook]?.complete();
delete this[hookSubject]?.[hook];
};
proto[hooksPatched][hook] = true;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return componentInstance[hookSubject][hook]!;
}
这样,对于给定的组件和给定的钩子键(ngOnInit
、ngOnChanges
等),我们构建了一个缓存层,以避免在再次请求时多次创建它.如果在组件中定义了原始钩子,我们将保存它,然后用猴子修补钩子以调用原始钩子+我们的代码来推送可观察对象中的值.
这是一张reminder of the Stackblitz美元.谢谢你的帮助.
编辑1:它看起来是这样的
编辑2:更奇怪的是,我做了minimal repro on Stackblitz,看看它是否能正常工作,在最低限度的复制中,它确实minimal repro on Stackblitz%地工作,但我无法确定差异.
事情就是这么简单
function patch(component: any) {
const original = component.constructor.prototype.ngOnChanges;
component.constructor.prototype.ngOnChanges = (changes: any) => {
original?.(changes);
console.log(`changes!`, JSON.stringify(changes, null, 2));
};
}
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
standalone: true,
})
export class ChildComponent {
@Input() value: any;
constructor() {
patch(this);
}
// in this minimal repro which very much looks like the implementation
// I've got in the full one, it works without having to have the ngOnChanges...
// ngOnChanges() {
// console.log('original')
// }
}
但是...我觉得这也是我在另一个突击队中所拥有的.