418 lines
16 KiB
Markdown
418 lines
16 KiB
Markdown
# NgZone
|
|
|
|
A zone is an execution context that persists across async tasks. You can think of it as [thread-local storage](https://en.wikipedia.org/wiki/Thread-local_storage) for JavaScript VMs.
|
|
This guide describes how to use Angular's NgZone to automatically detect changes in the component to update HTML.
|
|
|
|
## Fundamentals of change detection
|
|
|
|
To understand the benefits of `NgZone`, it is important to have a clear grasp of what change detection is and how it works.
|
|
|
|
### Displaying and updating data in Angular
|
|
|
|
In Angular, you can display data by binding controls in an HTML template to the properties of an Angular component.
|
|
|
|
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts"></code-example>
|
|
|
|
In addition, you can bind DOM events to a method of an Angular component. In such methods, you can also update a property of the Angular component, which updates the corresponding data displayed in the template.
|
|
|
|
<code-example path="user-input/src/app/click-me.component.ts" region="click-me-component" header="src/app/click-me.component.ts"></code-example>
|
|
|
|
In both of the above examples, the component's code updates only the property of the component.
|
|
However, the HTML is also updated automatically.
|
|
This guide describes how and when Angular renders the HTML based on the data from the Angular component.
|
|
|
|
|
|
### Detecting changes with plain JavaScript
|
|
|
|
To clarify how changes are detected and values updated, consider the following code written in plain JavaScript.
|
|
|
|
```javascript
|
|
<html>
|
|
<div id="dataDiv"></div>
|
|
<button id="btn">updateData</button>
|
|
<canvas id="canvas"></canvas>
|
|
<script>
|
|
let value = 'initialValue';
|
|
// initial rendering
|
|
detectChange();
|
|
|
|
function renderHTML() {
|
|
document.getElementById('dataDiv').innerText = value;
|
|
}
|
|
|
|
function detectChange() {
|
|
const currentValue = document.getElementById('dataDiv').innerText;
|
|
if (currentValue !== value) {
|
|
renderHTML();
|
|
}
|
|
}
|
|
|
|
// Example 1: update data inside button click event handler
|
|
document.getElementById('btn').addEventListener('click', () => {
|
|
// update value
|
|
value = 'button update value';
|
|
// call detectChange manually
|
|
detectChange();
|
|
});
|
|
|
|
// Example 2: HTTP Request
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.addEventListener('load', function() {
|
|
// get response from server
|
|
value = this.responseText;
|
|
// call detectChange manually
|
|
detectChange();
|
|
});
|
|
xhr.open('GET', serverUrl);
|
|
xhr.send();
|
|
|
|
// Example 3: setTimeout
|
|
setTimeout(() => {
|
|
// update value inside setTimeout callback
|
|
value = 'timeout update value';
|
|
// call detectChange manually
|
|
detectChange();
|
|
}, 100);
|
|
|
|
// Example 4: Promise.then
|
|
Promise.resolve('promise resolved a value').then(v => {
|
|
// update value inside Promise thenCallback
|
|
value = v;
|
|
// call detectChange manually
|
|
detectChange();
|
|
}, 100);
|
|
|
|
// Example 5: some other asynchronous APIs
|
|
document.getElementById('canvas').toBlob(blob => {
|
|
// update value when blob data is created from the canvas
|
|
value = `value updated by canvas, size is ${blob.size}`;
|
|
// call detectChange manually
|
|
detectChange();
|
|
});
|
|
</script>
|
|
</html>
|
|
```
|
|
|
|
After you update the data, you need to call `detectChange()` manually to check whether the data changed.
|
|
If the data changed, you render the HTML to reflect the updated data.
|
|
|
|
In Angular, this step is unnecessary. Whenever you update the data, your HTML is updated automatically.
|
|
|
|
### When apps update HTML
|
|
|
|
To understand how change detection works, first consider when the application needs to update the HTML. Typically, updates occur for one of the following reasons:
|
|
|
|
1. Component initialization. For example, when bootstrapping an Angular application, Angular loads the bootstrap component and triggers the [ApplicationRef.tick()](api/core/ApplicationRef#tick) to call change detection and View Rendering.
|
|
|
|
1. Event listener. The DOM event listener can update the data in an Angular component and also trigger change detection, as in the following example.
|
|
|
|
<code-example path="user-input/src/app/click-me.component.ts" region="click-me-component" header="src/app/click-me.component.ts"></code-example>
|
|
|
|
3. HTTP Data Request. You can also get data from a server through an HTTP request. For example:
|
|
|
|
```typescript
|
|
@Component({
|
|
selector: 'app-root',
|
|
template: '<div>{{data}}</div>';
|
|
})
|
|
export class AppComponent implements OnInit {
|
|
data = 'initial value';
|
|
serverUrl = 'SERVER_URL';
|
|
constructor(private httpClient: HttpClient) {}
|
|
|
|
ngOnInit() {
|
|
this.httpClient.get(this.serverUrl).subscribe(response => {
|
|
// user does not need to trigger change detection manually
|
|
this.data = response.data;
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
4. MacroTasks, such as `setTimeout()` or `setInterval()`. You can also update the data in the callback function of a `macroTask` such as `setTimeout()`. For example:
|
|
|
|
```typescript
|
|
@Component({
|
|
selector: 'app-root',
|
|
template: '<div>{{data}}</div>';
|
|
})
|
|
export class AppComponent implements OnInit {
|
|
data = 'initial value';
|
|
|
|
ngOnInit() {
|
|
setTimeout(() => {
|
|
// user does not need to trigger change detection manually
|
|
this.data = 'value updated';
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
5. MicroTasks, such as `Promise.then()`. Other asynchronous APIs return a Promise object (such as `fetch`), so the `then()` callback function can also update the data. For example:
|
|
|
|
```typescript
|
|
@Component({
|
|
selector: 'app-root',
|
|
template: '<div>{{data}}</div>';
|
|
})
|
|
export class AppComponent implements OnInit {
|
|
data = 'initial value';
|
|
|
|
ngOnInit() {
|
|
Promise.resolve(1).then(v => {
|
|
// user does not need to trigger change detection manually
|
|
this.data = v;
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
6. Other async operations. In addition to `addEventListener()`, `setTimeout()` and `Promise.then()`, there are other operations that can update the data asynchronously. Some examples include `WebSocket.onmessage()` and `Canvas.toBlob()`.
|
|
|
|
The preceding list contains most common scenarios in which the application might change the data. Angular runs change detection whenever it detects that data could have changed.
|
|
The result of change detection is that the DOM is updated with new data. Angular detects the changes in different ways. For component initialization, Angular calls change detection explicitly. For [asynchronous operations](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous), Angular uses a zone to detect changes in places where the data could have possibly mutated and it runs change detection automatically.
|
|
|
|
|
|
## Zones and execution contexts
|
|
|
|
A zone provides an execution context that persists across async tasks. [Execution Context](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) is an abstract concept that holds information about the environment within the current code being executed. Consider the following example:
|
|
|
|
```javascript
|
|
const callback = function() {
|
|
console.log('setTimeout callback context is', this);
|
|
}
|
|
|
|
const ctx1 = { name: 'ctx1' };
|
|
const ctx2 = { name: 'ctx2' };
|
|
|
|
const func = function() {
|
|
console.log('caller context is', this);
|
|
setTimeout(callback);
|
|
}
|
|
|
|
func.apply(ctx1);
|
|
func.apply(ctx2);
|
|
```
|
|
|
|
The value of `this` in the callback of `setTimeout()` might differ depending on when `setTimeout()` is called.
|
|
Thus, you can lose the context in asynchronous operations.
|
|
|
|
A zone provides a new zone context other than `this`, the zone context that persists across asynchronous operations.
|
|
In the following example, the new zone context is called `zoneThis`.
|
|
|
|
```javascript
|
|
zone.run(() => {
|
|
// now you are in a zone
|
|
expect(zoneThis).toBe(zone);
|
|
setTimeout(function() {
|
|
// the zoneThis context will be the same zone
|
|
// when the setTimeout is scheduled
|
|
expect(zoneThis).toBe(zone);
|
|
});
|
|
});
|
|
```
|
|
|
|
This new context, `zoneThis`, can be retrieved from the `setTimeout()` callback function, and this context is the same when the `setTimeout()` is scheduled.
|
|
To get the context, you can call [`Zone.current`](https://github.com/angular/angular/blob/master/packages/zone.js/lib/zone.ts).
|
|
|
|
## Zones and async lifecycle hooks
|
|
|
|
Zone.js can create contexts that persist across asynchronous operations as well as provide lifecycle hooks for asynchronous operations.
|
|
|
|
```javascript
|
|
const zone = Zone.current.fork({
|
|
name: 'zone',
|
|
onScheduleTask: function(delegate, curr, target, task) {
|
|
console.log('new task is scheduled:', task.type, task.source);
|
|
return delegate.scheduleTask(target, task);
|
|
},
|
|
onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
|
|
console.log('task will be invoked:', task.type, task.source);
|
|
return delegate.invokeTask(target, task, applyThis, applyArgs);
|
|
},
|
|
onHasTask: function(delegate, curr, target, hasTaskState) {
|
|
console.log('task state changed in the zone:', hasTaskState);
|
|
return delegate.hasTask(target, hasTaskState);
|
|
},
|
|
onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs) {
|
|
console.log('the callback will be invoked:', callback);
|
|
return delegate.invoke(target, callback, applyThis, applyArgs);
|
|
}
|
|
});
|
|
zone.run(() => {
|
|
setTimeout(() => {
|
|
console.log('timeout callback is invoked.');
|
|
});
|
|
});
|
|
```
|
|
|
|
The above example creates a zone with several hooks.
|
|
|
|
The `onXXXTask` hooks trigger when the status of the task changes.
|
|
The concept of a *Zone Task* is very similar to the JavaScript VM Task concept:
|
|
- `macroTask`: such as `setTimeout()`
|
|
- `microTask`: such as `Promise.then()`
|
|
- `eventTask`: such as `element.addEventListener()`
|
|
|
|
These hooks trigger under the following circumstances:
|
|
|
|
- `onScheduleTask`: triggers when a new asynchronous task is scheduled, such as when you call `setTimeout()`.
|
|
- `onInvokeTask`: triggers when an asynchronous task is about to execute, such as when the callback of `setTimeout()` is about to execute.
|
|
- `onHasTask`: triggers when the status of one kind of task inside a zone changes from stable to unstable or from unstable to stable. A status of "stable" means there are no tasks inside the zone, while "unstable" means a new task is scheduled in the zone.
|
|
- `onInvoke`: triggers when a synchronous function is going to execute in the zone.
|
|
|
|
With these hooks, `Zone` can monitor the status of all synchronous and asynchronous operations inside a zone.
|
|
|
|
The above example returns the following output:
|
|
|
|
```
|
|
the callback will be invoked: () => {
|
|
setTimeout(() => {
|
|
console.log('timeout callback is invoked.');
|
|
});
|
|
}
|
|
new task is scheduled: macroTask setTimeout
|
|
task state changed in the zone: { microTask: false,
|
|
macroTask: true,
|
|
eventTask: false,
|
|
change: 'macroTask' }
|
|
task will be invoked macroTask: setTimeout
|
|
timeout callback is invoked.
|
|
task state changed in the zone: { microTask: false,
|
|
macroTask: false,
|
|
eventTask: false,
|
|
change: 'macroTask' }
|
|
```
|
|
|
|
All of the functions of `Zone` are provided by a library called [Zone.js](https://github.com/angular/angular/tree/master/packages/zone.js/README.md).
|
|
This library implements those features by intercepting asynchronous APIs through monkey patching.
|
|
Monkey patching is a technique to add or modify the default behavior of a function at runtime without changing the source code.
|
|
|
|
## NgZone
|
|
|
|
While Zone.js can monitor all the states of synchronous and asynchronous operations, Angular additionally provides a service called NgZone.
|
|
This service creates a zone named `angular` to automatically trigger change detection when the following conditions are satisfied:
|
|
|
|
1. When a sync or async function is executed.
|
|
1. When there is no `microTask` scheduled.
|
|
|
|
### NgZone `run()` and `runOutsideOfAngular()`
|
|
|
|
`Zone` handles most asynchronous APIs such as `setTimeout()`, `Promise.then()`, and `addEventListener()`.
|
|
For the full list, see the [Zone Module document](https://github.com/angular/angular/blob/master/packages/zone.js/MODULE.md).
|
|
Therefore in those asynchronous APIs, you don't need to trigger change detection manually.
|
|
|
|
There are still some third party APIs that Zone does not handle.
|
|
In those cases, the `NgZone` service provides a [`run()`](api/core/NgZone#run) method that allows you to execute a function inside the Angular zone.
|
|
This function, and all asynchronous operations in that function, trigger change detection automatically at the correct time.
|
|
|
|
```typescript
|
|
export class AppComponent implements OnInit {
|
|
constructor(private ngZone: NgZone) {}
|
|
ngOnInit() {
|
|
// New async API is not handled by Zone, so you need to
|
|
// use ngZone.run() to make the asynchronous operation in the Angular zone
|
|
// and trigger change detection automatically.
|
|
this.ngZone.run(() => {
|
|
someNewAsyncAPI(() => {
|
|
// update the data of the component
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
By default, all asynchronous operations are inside the Angular zone, which triggers change detection automatically.
|
|
Another common case is when you don't want to trigger change detection.
|
|
In that situation, you can use another `NgZone` method: [`runOutsideAngular()`](api/core/NgZone#runoutsideangular).
|
|
|
|
```typescript
|
|
export class AppComponent implements OnInit {
|
|
constructor(private ngZone: NgZone) {}
|
|
ngOnInit() {
|
|
// You know no data will be updated,
|
|
// so you don't want to trigger change detection in this
|
|
// specified operation. Instead, call ngZone.runOutsideAngular()
|
|
this.ngZone.runOutsideAngular(() => {
|
|
setTimeout(() => {
|
|
// update component data
|
|
// but don't trigger change detection.
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Setting up Zone.js
|
|
|
|
To make Zone.js available in Angular, you need to import the `zone.js` package.
|
|
If you are using the Angular CLI, this step is done automatically, and you will see the following line in the `src/polyfills.ts`:
|
|
|
|
```typescript
|
|
/***************************************************************************************************
|
|
* Zone JS is required by default for Angular itself.
|
|
*/
|
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|
```
|
|
|
|
Before importing the `zone.js` package, you can set the following configurations:
|
|
|
|
- You can disable some asynchronous API monkey patching for better performance.
|
|
For example, you can disable the `requestAnimationFrame()` monkey patch, so the callback of `requestAnimationFrame()` will not trigger change detection.
|
|
This is useful if, in your application, the callback of the `requestAnimationFrame()` will not update any data.
|
|
- You can specify that certain DOM events do not run inside the Angular zone; for example, to prevent a `mousemove` or `scroll` event to trigger change detection.
|
|
|
|
There are several other settings you can change.
|
|
To make these changes, you need to create a `zone-flags.ts` file, such as the following.
|
|
|
|
```typescript
|
|
// disable patching requestAnimationFrame
|
|
(window as any).__Zone_disable_requestAnimationFrame = true;
|
|
|
|
// disable patching specified eventNames
|
|
(window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove'];
|
|
```
|
|
|
|
Next, import `zone-flags` before you import `zone.js` in the `polyfills.ts`:
|
|
|
|
```typescript
|
|
/***************************************************************************************************
|
|
* Zone JS is required by default for Angular.
|
|
*/
|
|
import `./zone-flags`;
|
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|
```
|
|
|
|
For more information about what you can configure, see the [Zone.js](https://github.com/angular/angular/tree/master/packages/zone.js) documentation.
|
|
|
|
### NoopZone
|
|
|
|
`Zone` helps Angular know when to trigger change detection and let the developers focus on the application development.
|
|
By default, `Zone` is loaded and works without additional configuration. However, you don't necessarily have to use `Zone` to make Angular work. Instead, you can opt to trigger change detection on your own.
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
<h4>Disabling <code>Zone</code></h4>
|
|
|
|
**If you disable `Zone`, you will need to trigger all change detection at the correct timing yourself, which requires comprehensive knowledge of change detection**.
|
|
|
|
</div>
|
|
|
|
To remove Zone.js, make the following changes.
|
|
|
|
1. Remove the `zone.js` import from `polyfills.ts`:
|
|
|
|
```typescript
|
|
/***************************************************************************************************
|
|
* Zone JS is required by default for Angular itself.
|
|
*/
|
|
// import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|
```
|
|
|
|
2. Bootstrap Angular with the `noop` zone in `src/main.ts`:
|
|
|
|
```typescript
|
|
platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' })
|
|
.catch(err => console.error(err));
|
|
```
|