停止 ngAfterContentInit 执行两次
Stop ngAfterContentInit from executing twice
我对为什么 ngAfterContentInit 在这种情况下执行两次感到有点困惑。我创建了一个精简版的应用程序来重现该错误。简而言之,我使用 *contentItem
标记组件,然后由 standard-layout
组件拾取这些组件进行渲染。只要我遵循这种模式,demo
的 ngAfterContentInit
就会执行两次。
我将演示应用程序放在 github 上,这将重现错误:
https://github.com/jVaaS/Whosebug/tree/master/ngaftercontentinit
否则这里是重要的部分:
越野车-app.dart:
@Component(
selector: "buggy-app",
template: """
<standard-layout>
<demo *contentItem></demo>
</standard-layout>
""",
directives: const [
ContentItemDirective,
StandardLayout,
Demo
]
)
class BuggyApp implements AfterContentInit {
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: BuggyApp");
}
}
标准-layout.dart:
////////////////////////////////////////////////////////////////////////////////
///
/// Standard Layout Component
/// <standard-layout></standard-layout>
///
@Component(
selector: "standard-layout",
template: """
<div *ngFor="let item of contentItems ?? []">
<template [ngTemplateOutlet]="item.template"></template>
</div>
""",
directives: const [ROUTER_DIRECTIVES, ContentItem])
class StandardLayout implements AfterContentInit {
@ContentChildren(ContentItemDirective)
QueryList<ContentItemDirective> contentItems;
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: StandardLayout");
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Directive
/// *contentItem
///
@Directive(selector: '[contentItem]')
class ContentItemDirective implements AfterContentInit {
final ViewContainerRef vcRef;
final TemplateRef template;
final ComponentResolver componentResolver;
ContentItemDirective(this.vcRef, this.template, this.componentResolver);
ComponentRef componentRef;
@override
ngAfterContentInit() async {
final componentFactory =
await componentResolver.resolveComponent(ContentItem);
componentRef = vcRef.createComponent(componentFactory);
(componentRef.instance as ContentItem)
..template = template;
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Generator
///
@Component(
selector: "content-item",
host: const {
'[class.content-item]': "true",
},
template: """
<template [ngTemplateOutlet]="template"></template>
""",
directives: const [NgTemplateOutlet]
)
class ContentItem {
TemplateRef template;
}
////////////////////////////////////////////////////////////////////////////////
最后是 demo.dart
:
@Component(
selector: "demo",
template: "Hello World Once, but demo prints twice!")
class Demo implements AfterContentInit {
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: Demo");
}
}
main.dart
内容不多:
void main() {
bootstrap(BuggyApp);
}
当我运行这个时,Hello World
按预期打印一次:
但是在查看终端时:
所以 demo
组件只渲染一次,但它的 ngAfterContentInit
被执行了两次,当你假设它只被执行一次时,这会造成严重破坏。
我已经尝试添加一个 hacky 解决方法,但似乎组件实际上被重新渲染了两次:
int counter = 0;
@override
ngAfterContentInit() {
if (counter == 0) {
print(">>> ngAfterContentInit: Demo");
counter++;
}
}
这是 Angular 中的错误还是我可以采取什么措施来防止这种情况?
pubspec.yaml
以备不时之需:
name: bugdemo
version: 0.0.0
description: Bug Demo
environment:
sdk: '>=1.13.0 <2.0.0'
dependencies:
angular2: 3.1.0
browser: ^0.10.0
http: any
js: ^0.6.0
dart_to_js_script_rewriter: ^1.0.1
transformers:
- angular2:
platform_directives:
- package:angular2/common.dart#COMMON_DIRECTIVES
platform_pipes:
- package:angular2/common.dart#COMMON_PIPES
entry_points: web/main.dart
- dart_to_js_script_rewriter
- $dart2js:
commandLineOptions: [--enable-experimental-mirrors]
和 index.html
很好的衡量标准:
<!DOCTYPE html>
<html>
<head>
<title>Strange Bug</title>
</head>
<body>
<buggy-app>
Loading ...
</buggy-app>
<script async src="main.dart" type="application/dart"></script>
<script async src="packages/browser/dart.js"></script>
</body>
</html>
更新
所以有人建议它可能只在开发模式下发生两次。我已经完成 pub build
和 运行 index.html
,现在 main.dart.js
包含 Chrome.
它仍在执行两次,用 ngAfterViewInit
试过,它也执行了两次。
可能与
有关
你在开发模式吗?控制台会在应用程序 bootstrap 时告诉您。在开发模式下,Angular 执行两次 changeDetection,因此它可以检测副作用。
这看起来不像是错误。
这是为 demo.dart
生成的代码:
https://gist.github.com/matanlurey/f311d90053e36cc09c6e28f28ab2d4cd
void detectChangesInternal() {
bool firstCheck = identical(this.cdState, ChangeDetectorState.NeverChecked);
final _ctx = ctx;
if (!import8.AppViewUtils.throwOnChanges) {
dbg(0, 0, 0);
if (firstCheck) {
_Demo_0_2.ngAfterContentInit();
}
}
_compView_0.detectChanges();
}
我很怀疑,所以我更改了您的打印消息以包含 hashCode
:
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: Demo $hashCode");
}
然后我看到了以下内容:
ngAfterContentInit: Demo 516575718
ngAfterContentInit: Demo 57032191
这意味着两个 不同的 演示实例是活的,而不是只有一个发射两次。然后我将 StackTrace.current
添加到 demo 的构造函数中以获取堆栈跟踪:
Demo() {
print('Created $this:$hashCode');
print(StackTrace.current);
}
并得到:
0 Demo.Demo (package:bugdemo/demo.dart:11:20)
1 ViewBuggyApp1.build (package:bugdemo/buggy-app.template.dart:136:21)
2 AppView.create (package:angular2/src/core/linker/app_view.dart:180:12)
3 DebugAppView.create (package:angular2/src/debug/debug_app_view.dart:73:26)
4 TemplateRef.createEmbeddedView (package:angular2/src/core/linker/template_ref.dart:27:10)
5 ViewContainer.createEmbeddedView (package:angular2/src/core/linker/view_container.dart:86:43)
反过来说您的组件是由 BuggyApp 的模板创建的:
<standard-layout>
<demo *contentItem></demo>
</standard-layout>
近一点。继续挖掘。
我注释掉了ContentItemDirective
,看到Demo现在创建了一次。我猜想由于 *contentItem
,您预计它根本不会被创建,所以继续挖掘 - 最终弄明白了。
您的 StandardLayout
组件创建模板:
<div *ngFor="let item of contentItems ?? []">
<template [ngTemplateOutlet]="item.template"></template>
</div>
但是您的 ContentItemDirective
通过 ComponentResolver 也是如此。我想你不是故意的。所以我会弄清楚你想做什么,并通过删除在这些地方之一创建组件来解决你的问题。
我对为什么 ngAfterContentInit 在这种情况下执行两次感到有点困惑。我创建了一个精简版的应用程序来重现该错误。简而言之,我使用 *contentItem
标记组件,然后由 standard-layout
组件拾取这些组件进行渲染。只要我遵循这种模式,demo
的 ngAfterContentInit
就会执行两次。
我将演示应用程序放在 github 上,这将重现错误: https://github.com/jVaaS/Whosebug/tree/master/ngaftercontentinit
否则这里是重要的部分:
越野车-app.dart:
@Component(
selector: "buggy-app",
template: """
<standard-layout>
<demo *contentItem></demo>
</standard-layout>
""",
directives: const [
ContentItemDirective,
StandardLayout,
Demo
]
)
class BuggyApp implements AfterContentInit {
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: BuggyApp");
}
}
标准-layout.dart:
////////////////////////////////////////////////////////////////////////////////
///
/// Standard Layout Component
/// <standard-layout></standard-layout>
///
@Component(
selector: "standard-layout",
template: """
<div *ngFor="let item of contentItems ?? []">
<template [ngTemplateOutlet]="item.template"></template>
</div>
""",
directives: const [ROUTER_DIRECTIVES, ContentItem])
class StandardLayout implements AfterContentInit {
@ContentChildren(ContentItemDirective)
QueryList<ContentItemDirective> contentItems;
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: StandardLayout");
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Directive
/// *contentItem
///
@Directive(selector: '[contentItem]')
class ContentItemDirective implements AfterContentInit {
final ViewContainerRef vcRef;
final TemplateRef template;
final ComponentResolver componentResolver;
ContentItemDirective(this.vcRef, this.template, this.componentResolver);
ComponentRef componentRef;
@override
ngAfterContentInit() async {
final componentFactory =
await componentResolver.resolveComponent(ContentItem);
componentRef = vcRef.createComponent(componentFactory);
(componentRef.instance as ContentItem)
..template = template;
}
}
////////////////////////////////////////////////////////////////////////////////
///
/// Content Item Generator
///
@Component(
selector: "content-item",
host: const {
'[class.content-item]': "true",
},
template: """
<template [ngTemplateOutlet]="template"></template>
""",
directives: const [NgTemplateOutlet]
)
class ContentItem {
TemplateRef template;
}
////////////////////////////////////////////////////////////////////////////////
最后是 demo.dart
:
@Component(
selector: "demo",
template: "Hello World Once, but demo prints twice!")
class Demo implements AfterContentInit {
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: Demo");
}
}
main.dart
内容不多:
void main() {
bootstrap(BuggyApp);
}
当我运行这个时,Hello World
按预期打印一次:
但是在查看终端时:
所以 demo
组件只渲染一次,但它的 ngAfterContentInit
被执行了两次,当你假设它只被执行一次时,这会造成严重破坏。
我已经尝试添加一个 hacky 解决方法,但似乎组件实际上被重新渲染了两次:
int counter = 0;
@override
ngAfterContentInit() {
if (counter == 0) {
print(">>> ngAfterContentInit: Demo");
counter++;
}
}
这是 Angular 中的错误还是我可以采取什么措施来防止这种情况?
pubspec.yaml
以备不时之需:
name: bugdemo
version: 0.0.0
description: Bug Demo
environment:
sdk: '>=1.13.0 <2.0.0'
dependencies:
angular2: 3.1.0
browser: ^0.10.0
http: any
js: ^0.6.0
dart_to_js_script_rewriter: ^1.0.1
transformers:
- angular2:
platform_directives:
- package:angular2/common.dart#COMMON_DIRECTIVES
platform_pipes:
- package:angular2/common.dart#COMMON_PIPES
entry_points: web/main.dart
- dart_to_js_script_rewriter
- $dart2js:
commandLineOptions: [--enable-experimental-mirrors]
和 index.html
很好的衡量标准:
<!DOCTYPE html>
<html>
<head>
<title>Strange Bug</title>
</head>
<body>
<buggy-app>
Loading ...
</buggy-app>
<script async src="main.dart" type="application/dart"></script>
<script async src="packages/browser/dart.js"></script>
</body>
</html>
更新
所以有人建议它可能只在开发模式下发生两次。我已经完成 pub build
和 运行 index.html
,现在 main.dart.js
包含 Chrome.
它仍在执行两次,用 ngAfterViewInit
试过,它也执行了两次。
可能与
你在开发模式吗?控制台会在应用程序 bootstrap 时告诉您。在开发模式下,Angular 执行两次 changeDetection,因此它可以检测副作用。
这看起来不像是错误。
这是为 demo.dart
生成的代码:
https://gist.github.com/matanlurey/f311d90053e36cc09c6e28f28ab2d4cd
void detectChangesInternal() {
bool firstCheck = identical(this.cdState, ChangeDetectorState.NeverChecked);
final _ctx = ctx;
if (!import8.AppViewUtils.throwOnChanges) {
dbg(0, 0, 0);
if (firstCheck) {
_Demo_0_2.ngAfterContentInit();
}
}
_compView_0.detectChanges();
}
我很怀疑,所以我更改了您的打印消息以包含 hashCode
:
@override
ngAfterContentInit() {
print(">>> ngAfterContentInit: Demo $hashCode");
}
然后我看到了以下内容:
ngAfterContentInit: Demo 516575718
ngAfterContentInit: Demo 57032191
这意味着两个 不同的 演示实例是活的,而不是只有一个发射两次。然后我将 StackTrace.current
添加到 demo 的构造函数中以获取堆栈跟踪:
Demo() {
print('Created $this:$hashCode');
print(StackTrace.current);
}
并得到:
0 Demo.Demo (package:bugdemo/demo.dart:11:20) 1 ViewBuggyApp1.build (package:bugdemo/buggy-app.template.dart:136:21) 2 AppView.create (package:angular2/src/core/linker/app_view.dart:180:12) 3 DebugAppView.create (package:angular2/src/debug/debug_app_view.dart:73:26) 4 TemplateRef.createEmbeddedView (package:angular2/src/core/linker/template_ref.dart:27:10) 5 ViewContainer.createEmbeddedView (package:angular2/src/core/linker/view_container.dart:86:43)
反过来说您的组件是由 BuggyApp 的模板创建的:
<standard-layout>
<demo *contentItem></demo>
</standard-layout>
近一点。继续挖掘。
我注释掉了ContentItemDirective
,看到Demo现在创建了一次。我猜想由于 *contentItem
,您预计它根本不会被创建,所以继续挖掘 - 最终弄明白了。
您的 StandardLayout
组件创建模板:
<div *ngFor="let item of contentItems ?? []">
<template [ngTemplateOutlet]="item.template"></template>
</div>
但是您的 ContentItemDirective
通过 ComponentResolver 也是如此。我想你不是故意的。所以我会弄清楚你想做什么,并通过删除在这些地方之一创建组件来解决你的问题。