Angular 2 Change Detection - 1

閱讀 Angular 6/RxJS 最新教程,請訪問 前端修仙之路

Change Detection (變化檢測) 是 Angular 2 中最重要的一個特性。當組件中的數據發生變化的時候,Angular 2 能檢測到數據變化並自動刷新視圖反映出相應的變化。javascript

在介紹變化檢測以前,咱們要先介紹一下瀏覽器中渲染的概念,渲染是將模型映射到視圖的過程。模型的值能夠是 JavaScript 中的原始數據類型、對象、數組或其餘數據對象。然而視圖能夠是頁面中的段落、表單、按鈕等其餘元素,這些頁面元素內部使用 DOM (Document Object Model) 來表示。css

圖片描述

爲了更好地理解,咱們來看一個具體的示例:html

<h4 id="greeting"></h4>
<script>
    document.getElementById("greeting").innerHTML = "Hello World!";
</script>

這個例子很簡單,由於模型不會變化,因此頁面只會渲染一次。若是數據模型在運行時會不斷變化,那麼整個過程將變得複雜。所以爲了保證數據與視圖的同步,頁面將會進行屢次渲染。接下來咱們來考慮一下如下幾個問題:前端

  • 何時模型會發生變化
  • 模型產生了什麼變化
  • 變化後須要更新的視圖區域在哪裏
  • 怎麼更新對應視圖區域

而變化檢測的基本目的就是解決上述問題。在 Angular 2 中當組件內的模型發生變化的時候,組件內的變化檢測器就會檢測到更新,而後通知視圖刷新。所以變化檢測器有兩個主要的任務:java

  • 檢測模型的變化
  • 通知視圖刷新

接下來咱們來分析一下什麼是變化,變化是怎麼產生的。git

變化和事件

變化是舊模型與新模型之間的區別,換句話說變化產生了一個新的模型。讓咱們來看一下下面的代碼:github

import { Component } from '@angular/core';

@Component({
  selector: 'exe-counter',
  template: `
  <p>當前值:{{ counter }}</p>
  <button (click)="countUp()"> + </button>`
})
export class CounterComponent {
  counter = 0;

  countUp() {
    this.counter++;
  }
}

頁面首次渲染完後,計數器的當前值爲0。當咱們點擊 + 按鈕時,計數器的 counter 值將會自動加1,以後頁面中當前值也會被更新。在這個例子中,點擊事件引發了 counter 屬性值的變化。web

咱們繼續看下一個例子:typescript

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'exe-counter',
  template: `
    <p>當前值:{{ counter }}</p>
  `
})
export class CounterComponent implements OnInit {
  counter = 0;

  ngOnInit() {
    setInterval(() => {
      this.counter++;
    }, 1000);
  }
}

該組件經過 setInterval 定時器,實現每秒鐘 counter 值自動加1。在這種狀況下,它是定時器事件引發了屬性值的變化。最後咱們再來看個例子:shell

import { Component, OnInit } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'exe-counter',
  template: `
    <p>當前值:{{ counter }}</p>
  `
})
export class CounterComponent implements OnInit {
  counter = 0;
  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/counter-data.json')
        .map(res => res.json())
        .subscribe(data => {
          this.counter = data.value;
        });
  }
}

該組件在進行初始化的時候,會發送一個 HTTP 請求去獲取初始值。當請求成功返回的時候,組件的 counter 屬性的值會被更新。在這種狀況下,它是由 XHR 回調引發了屬性值的變化。

如今咱們來總結一下,引發模型變化的三類事件源:

  • Events:click, mouseover, keyup ...
  • Timers:setInterval、setTimeout
  • XHRs:Ajax(GET、POST ...)

這些事件源有一個共同的特性,即它們都是異步操做。那咱們能夠這樣認爲,全部的異步操做都有可能會引發模型的變化。

很是好,你已經瞭解了引發模型變化的事件源和觸發變化的時機點。可是你還不知道,是由誰來負責通知相應的變化給視圖。接下來,咱們將討論一種容許 Angular 隨時檢測到變化的機制,它被稱爲 Zone

Zones

Zone 是下一個 ECMAScript 規範的建議之一。Angular 團隊實現了 JavaScript 版本的 zone.js ,它是用於攔截和跟蹤異步工做的機制。

Zone 是一個全局的對象,用來配置有關如何攔截和跟蹤異步回調的規則。Zone 有如下能力:

  • 攔截異步任務調度
  • 提供了將數據附加到 zones 的方法
  • 爲異常處理函數提供正確的上下文
  • 攔截阻塞的方法,如 alert、confirm 方法

咱們來看一個簡單的示例:

Zone.current.fork({}).run(function () {
    Zone.current.inTheZone = true;
  
    setTimeout(function () {
        console.log('in the zone: ' + !!Zone.current.inTheZone); 
    }, 0);
});

console.log('in the zone: ' + !!Zone.current.inTheZone);

以上代碼運行後的結果是:

in the zone: false
in the zone: true

是否是感受很神奇!在Angular 2 中,有一個 NgZone,它是專門爲 Angular 2 定製的 zone。在正式介紹它以前,咱們先來看一下 Angular 1.x 中的一個例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Angular 1.x Demo</title>
    <script src="//cdn.bootcss.com/angular.js/1.6.3/angular.min.js"></script>
</head>
<body ng-app="exeApp">
<div ng-controller="MainCtrl">
    <h4>Hello {{ name }}</h4>
</div>
<script type="text/javascript">
    angular.module('exeApp', [])
            .controller('MainCtrl', ['$scope', function ($scope) {
                $scope.name = 'Angular';

                setTimeout(function () {
                    $scope.name = 'Angular 2';
                }, 2000);
            }]);
</script>
</body>
</html>

以上代碼運行後的輸出結果:

圖片描述

用過 Angular 1.x 的同窗,應該很清楚能夠經過 Angular 1.x 中的 $timeout 服務或手動調用 $scope.$digest() 方法來通知視圖刷新。這對初學者來講,是很麻煩的一件事情。大家應該還記得前面計數器組件的例子,咱們經過 setInterval 定時器,實現每秒鐘 counter 值自動加1,頁面就自動刷新了。不須要再使用 Angular 1.x 中的 $timeout 服務或手動調用 $scope.$digest() 方法來刷新視圖。

爲何咱們都是使用定時器,而在 Angular 2 中模型發生變化後,卻能自動通知視圖進行刷新呢 ?咱們來分析一下,首先在瀏覽器中新開一個 Tab 頁,在控制檯輸入:

window.setTimeout.toString() 
"function setTimeout() { [native code] }"

而後再打開一個 Angular 2 應用的頁面,在控制檯一樣輸入:

window.setTimeout.toString()
"function setTimeout(){return f(this, arguments)}"

咱們發如今 Angular 2 中,setTimeout 方法已經被重寫了,最簡單的實現方式以下:

var originSetTimeout = window.setTimeout;
window.setTimeout = function(fn, delay) {
  console.log('setTimeout has been called');
  originSetTimeout(fn, delay); 
}

其實在 Angular 2 應用程序啓動以前,Zone 採用猴子補丁 (Monkey-patched) 的方式,將 JavaScript 中的異步任務都進行了包裝,這使得這些異步任務都能運行在 Zone 的執行上下文中,每一個異步任務在 Zone 中都是一個任務,除了提供了一些供開發者使用的鉤子外,默認狀況下 Zone 重寫了如下方法:

  • setInterval、clearInterval、setTimeout、clearTimeout
  • alert、prompt、confirm
  • requestAnimationFrame、cancelAnimationFrame
  • addEventListener、removeEventListener

Zone 內部源碼片斷:

var set = 'set';
var clear = 'clear';
var blockingMethods = ['alert', 'prompt', 'confirm'];
var _global = typeof window === 'object' && window || 
     typeof self === 'object' && self || global;
patchTimer(_global, set, clear, 'Timeout');
patchTimer(_global, set, clear, 'Interval');
patchTimer(_global, set, clear, 'Immediate');
patchTimer(_global, 'request', 'cancel', 'AnimationFrame');
patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame');
patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame');

NgZone

NgZone 是基於 Zone 實現的,它是Zone派生出來的一個子Zone,在 Angular 環境內註冊的異步事件都運行在這個子 Zone 內 (由於NgZone擁有整個運行環境的執行上下文),它擴展了自有的一些 API 並添加了一些功能性的方法到它的執行上下文中。

在 Angular 源碼中,有一個 ApplicationRef_ 類,其做用是用來監聽 NgZone 中的 onMicrotaskEmpty 事件,不管什麼時候只要觸發這個事件,那麼將會執行一個 tick 方法用來告訴 Angular 去執行變化檢測,簡化版的代碼以下:

class ApplicationRef { 
  private _views: InternalViewRef[] = [];

  constructor(private zone: NgZone) {
        this.zone.onMicrotaskEmpty.subscribe(() => {
            this.zone.run(() => { 
              this.tick();
            }); 
        });
  }
  
  tick() { 
    if (this._runningTick) {
      throw new Error('ApplicationRef.tick is called recursively');
    }
    this._views.forEach((view) => view.detectChanges());
   }
}

如今咱們先來總結一下前面所講的內容:

  • 異步操做被安排爲任務
  • Zones 跟蹤任務的執行
  • Angular 處理由執行異步操做引發的事件
  • Angular 對全部組件執行變化檢測,若發生變化則更新視圖

要徹底理解 Zone 的工做原理是比較困難的,對咱們大部分的人來講,只要知道 Angular 內部是經過它來跟蹤異步任務,而後執行變化檢測任務就能夠了。

我有話說

1.在 Angular 2 項目中怎麼訪問 Zone 打補丁前的方法,如 setTimeout、clearTimeout 等

由於 Zone 內部經過內建的 __symbol__ 函數來模擬 Symbol

function __symbol__(name) {
  return '__zone_symbol__' + name;
}

所以咱們能夠在瀏覽器的控制檯中運行:

Object.keys(window).forEach((key) => {
 if(key.indexOf('zone_symbol') > 0) {
    console.log(key);
 }  
});

運行後控制檯的輸出結果以下:

圖片描述

2.前面介紹 Zone 使用的示例,爲何控制檯會輸出那樣的結果 ?

// 加載Zone.js給瀏覽器中的一些異步操做打上補丁
// 建立Root Zone
// 調用Zone.current對象上的fork方法建立新的zone,咱們稱之爲childZone
Zone.current.fork({}).run(function () { 
      // 運行run方法,Zone.current被設置爲函數被執行時所屬的Zone,即childZone
    Zone.current.inTheZone = true;
  
      // 這裏註冊了一個定時器。因爲被打過了猴子補丁,這裏調用的並非
    // 瀏覽器"默認"的setTimeout方法。所以,這裏其實是在配置代理。這裏
    // 要重點指出的是這個代理會保留一個指向建立時所屬Zone的引用即childZone,
    // 稍後會用到這個引用。
    setTimeout(function () {
        // 定時時間到,此時的Zone.current的值會被重置爲childZone
        console.log('in the zone: ' + !!Zone.current.inTheZone); 
    }, 0);
  
      // 代碼執行完 Zone.current屬性被重置爲Root Zone
    // Zone的生命週期裏的鉤子函數會被觸發
});

console.log('in the zone: ' + !!Zone.current.inTheZone);

若是仍是很差理解的話,咱們能夠想象一下同步的過程:

const rootZone = Zone.current;
// 建立一個新的Zone
const childZone = Zone.current.fork({});
// 設置當前的zone
Zone.current = zone;
// 爲當前的zone添加inTheZone屬性
Zone.current.inTheZone = true;
console.log('in the zone: ' + !!Zone.current.inTheZone);
// 退出當前的zone
Zone.current = rootZone;
console.log('in the zone: ' + !!Zone.current.inTheZone);

總結

這篇文章咱們先介紹了瀏覽器中渲染的概念,而後經過三個示例引出了引發模型變化的事件源並總結了它們之間的共性,此外咱們還介紹了 Angular 1.x 項目中初學者容易遇到的問題,並基於該問題引入了 Zone 和 NgZone 的概念,最後咱們簡單介紹了 Zone.js 的內部工做原理。下一篇文章咱們將詳細介紹 Angular 2 組件中的變化檢測器。

相關文章
相關標籤/搜索