我们在工作中建立了一个图书馆,名为ngx-observable-lifecycle.

它基本上挂接到Angular 组件(ngOnInit、ngOnChanges等)的所有本机钩子,然后给出一个对象,其中每个键都是给定钩子的名称,值是调用该钩子的时间的可观察值.

可以把它看作一个小的包装器实用程序,它能够以一种更具react 性的方式工作,而不必手动创建Subject并在挂钩发生时进行推送.

这一切都运行得很好,except在一个非常、非常、非常具体的 case 中:ngOnChanges.

我们构建了一个简单的演示,其中父组件定义了一个计数器,单击按钮使其递增,然后将该计数器传递给子组件的输入.这里没什么花哨的.

然后,我们使用库从子组件中挂钩ngOnChanges并订阅可观察对象.

  • 启动应用程序时,会设置初始计数器值,订阅者会通知我们收到了预期的计数器值
  • 当我们递增时,ngOnChanges不会发生,这是这里的主要问题.我能看到doCheckafterContentCheckedafterViewChecked,但看不到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]!;
}

这样,对于给定的组件和给定的钩子键(ngOnInitngOnChanges等),我们构建了一个缓存层,以避免在再次请求时多次创建它.如果在组件中定义了原始钩子,我们将保存它,然后用猴子修补钩子以调用原始钩子+我们的代码来推送可观察对象中的值.

这是一张reminder of the Stackblitz美元.谢谢你的帮助.

编辑1:它看起来是这样的

Demo of the issue

编辑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')
  // }
}

但是...我觉得这也是我在另一个突击队中所拥有的.

推荐答案

我在Angular 框架上做了一些工作,所以我可以给你一些见解.

ngOnChanges不仅仅是组件上的常规方法.

当angular编译器检测到组件使用了这个钩子时,它会用ngOnChanges生命周期钩子来装饰组件.

生成的JS看起来有点像下面这样:

static ɵcmp = defineComponent({
   ...
   inputs: {name: 'publicName'},
   features: [NgOnChangesFeature]
 });

没有进入细节,没有它会发生什么,钩子在setInput之前被调用.

这就是为什么,在第一个循环上您不会得到任何更改(该值尚未递增).在第二次单击时,您会得到更改,但值是之前的值.

因此,onchange和您在模板上看到的值之间始终存在差异.

您可以通过在rememberChangeHistoryAndInvokeOnChangesHookngOnChangesSetInput上都在ng_onchanges_feature.ts中添加断点来轻松地重现这一点.

要向框架文件添加断点,请确保在您的angular.json:

          "sourceMap": {
            "scripts": true,
            "vendor": true
          }

Source code reference.

Javascript相关问答推荐

是什么原因导致此Angular 16电影应用程序中因类型错误而不存在属性?

使用useup时,React-Redux无法找到Redux上下文值

Redux工具包查询(RTKQ)端点无效并重新验证多次触发

窗口.getComputedStyle()在MutationObserver中不起作用

Plotly热图:在矩形上zoom 后将zoom 区域居中

单击ImageListItemBar的IconButton后,在Material—UI对话框中显示图像

手机上的渲染错误文本必须在文本组件中渲染,但在浏览器上没有问题<><>

如何用拉威尔惯性Vue依赖下拉?

setcallback是什么时候放到macrotask队列上的?

你怎么看啦啦队的回应?

无法访问Vue 3深度监视器中对象数组的特定对象值'

让chart.js饼图中的一个切片变厚?

Puppeteer上每页的useProxy返回的不是函数/构造函数

MarkLogic-earch.suggest不返回任何值

如何使用puppeteer操作所有选项

在将元素追加到DOM之前,createElement()是否会触发回流?混淆abt DocumentFragment行为

我们是否可以在reactjs中创建多个同名的路由

如何用react组件替换dom元素?

ReactJS在类组件中更新上下文

如何创建一个for循环,用于计算仪器刻度长度并将其放入一个HTML表中?