AngularJS 中的髒資料檢查

Rana Hasnain Khan 2022年5月31日
AngularJS 中的髒資料檢查

我們將介紹如何在 AngularJs 中進行髒資料檢查。

在 AngularJS 中實現髒資料檢查

AngularJS 對雙向資料繫結的 $scope 變數執行髒資料檢查。Ember.js 通過以程式設計方式修復 setter 和 getter 來執行雙向資料繫結,但在 Angular 中,髒資料檢查允許它查詢可能可用或不可用的變數。

# AngularJs
$scope.$watch( wExp, listener, objEq );

$scope.$watch 函式用於我們想要檢視變數何時被更改。我們必須選擇三個引數來應用這個函式:wExp 是我們想要觀看的內容,listener 是我們希望它在更新時執行的操作,以及我們想要檢查變數還是物件。

當我們想要檢查一個變數時,我們可以在使用這個函式時跳過這個。讓我們看一個例子,如下所示。

#AngularJs
$scope.newName = 'Subhan';

$scope.$watch( function( ) {
    return $scope.name;
}, function( newVal, oldVal ) {
    console.log('$scope.newName is updated!');
} );

Angular 會將你的 watcher 函式儲存在 $scope 中。你可以通過將 $scope 記錄到控制檯來檢查這一點。

$watch 中,我們還可以使用字串代替函式。這也將與函式的工作方式相同。

當我們在 Angular 原始碼中新增一個字串時,程式碼將是:

#AngularJs
if (typeof wExp == 'string' && get.constant) {
  var newFn = watcher.fn;
  watcher.fn = function(newVal, oldVal, scope) {
    newFn.call(this, newVal, oldVal, scope);
    arrayRemove(array, watcher);
  };
}

通過應用此程式碼,wExp 將被設定為一個函式。這將使用具有我們選擇的名稱的變數指定我們的偵聽器。

AngularJS 中的 $watchers 函式

所有選定的觀察者都將儲存在 $scope 中的 $$watchers 變數中。當我們檢查 $$watchers 時,你會發現一個物件陣列,如下所示。

#AngularJs
$$watchers = [
    {
        eq: false,
        fn: function( newVal, oldVal ) {},
        last: 'Subhan',
        exp: function(){},
        get: function(){}
    }
];

unRegWatch 函式由 $watch 函式返回。這表明如果我們想將初始的 $scope.$watch 分配給一個變數,我們可以通過命令它停止觀察來輕鬆實現這一點。

只需確認我們已經開啟並檢視了在斷開觀察程式之前記錄的第一個 $scope

AngularJS 中的 $scope.$apply 函式

當我們嘗試執行 controller/directive/etc. 時,Angular 會執行一個函式,我們在其中呼叫 $scope.$watch。在管理 rootScope 中的 $digest 函式之前,$apply 函式將執行我們選擇的函式。

Angular $apply 函式如下:

#AngularJs
$apply: function(expr) {
    try {
      beginPhase('$apply');
      return this.$eval(expr);
    } catch (e) {
      $exceptionHandler(e);
    } finally {
      clearPhase();
      try {
        $rootScope.$digest();
      } catch (e) {
        $exceptionHandler(e);
        throw e;
      }
    }
}

AngularJS 中的 expr 引數

當使用 $scope.$apply 時,有時 Angular 或者我們會通過一個函式,expr 引數就是那個函式。

在使用此功能時,我們不會發現需要最頻繁地使用 $apply。讓我們看看當 ng-keydown 使用 $scope.$apply 時會發生什麼。

要提交指令,我們使用以下程式碼。

#AngularJs
var EventNgDirective = {};
forEach(
  keydown keyup keypress submit focus blur copy cut paste'.split(' '),
  function(newName) {
    var newDirectiveName = newDirectiveNormalize('ng-' + newName);
    EventNgDirective[newDirectiveName] = ['$parse', function($parse) {
      return {
        compile: function($element, attr) {
          var fn = $parse(attr[directiveName]);
          return function ngEventHandler(scope, element) {
            element.on(lowercase(newName), function(event) {
              scope.$apply(function() {
                fn(scope, {$event:event});
              });
            });
          };
        }
      };
    }];
  }
);

此程式碼將遍歷可以觸發的不同型別的事件,生成一個名為 ng(eventName) 的新指令。當涉及指令的編譯功能時,事件處理程式被記錄在元素上,其中事件包含指令的名稱。

當這個事件被觸發時,Angular 將執行 $scope.$apply,為其提供一個執行函式。

這就是 $scope 值將如何使用元素值更新的方式,但此繫結只是單向繫結。這樣做的原因是我們應用了 ng-keydown,它允許我們僅在應用此事件時進行更改。

結果,得到了一個新的值!

我們的主要目標是實現雙向資料繫結。為了實現這個目標,我們可以使用 ng-model

為了使 ng-model 發揮作用,它同時使用 $scope.$watch$scope.$apply

我們給出的輸入將通過 ng-model 加入事件處理程式。此時,呼叫了 $scope.$watch

$scope.$watch 在指令的控制器中被呼叫。

#AngularJs
$scope.$watch(function ngModelWatch() {
    var value = ngModelGet($scope);

    if (ctrl.$modelValue !== value) {

        var formatters = ctrl.$formatters,
            idx = formatters.length;

        ctrl.$modelValue = value;
        while(idx--) {
            value = formatters[idx](value);
        }

        if (ctrl.$viewValue !== value) {
            ctrl.$viewValue = value;
            ctrl.$render();
        }
    }

    return value;
});

當設定 $scope.$watch 時僅使用一個引數時,我們選擇的函式將被應用,即使它是否已更新。ng-model 中的函式檢查模型和檢視是否同步。

如果不同步,該函式會給模型一個新值並更新它。當我們在 $digest 中執行這個函式時,這個函式允許我們通過返回新值來知道新值是什麼。

為什麼監聽器沒有被觸發

讓我們回到示例中的點,我們在與我們描述的類似的函式中取消註冊了 $scope.$watch 函式。我們現在可以承認沒有收到有關更新 $scope.name 的通知的原因,即使我們已經更新了它。

正如我們所知,$scope.$apply 由 Angular 在每個指令控制器函式上執行。當我們估計了指令的控制器功能時,$scope.$apply 函式將僅在一個條件下執行 $digest

這意味著我們在 $scope.$watch 函式能夠執行之前取消了它的註冊,因此它被呼叫的可能性很小甚至沒有。

AngularJS 中的 $digest 函式

你可以通過 $scope.$apply$rootScope 上呼叫此函式。我們可以在 $rootScope 上執行摘要迴圈,然後它將越過範圍並執行摘要迴圈。

摘要迴圈將觸發我們在 $$watchers 變數中的所有 wExp 函式。它將根據最後一個已知值檢查它們。

如果結果是否定的,監聽器將被觸發。

在摘要迴圈中,當它執行時,它會迴圈兩次。一次,它將在觀察者之間迴圈,然後再次迴圈,直到迴圈不再是

wExp 和最後一個已知值不相等時,我們說迴圈是髒的。通常這個迴圈會執行一次,如果執行十次以上就會報錯。

Angular 可以在任何可能改變模型值的東西上執行 $scope.$apply

你必須執行 $scope.$apply(); 當我們在 Angular 之外更新了 $scope。這將通知 Angular 範圍已更新。

讓我們設計一個基本版本的髒資料檢查。

我們希望儲存的所有資料都將在 Scope 函式中。我們將在函式上擴充套件物件以複製 $digest$watch

因為我們不需要評估與 Scope 有關的函式,所以我們不會使用 $apply。我們將直接使用 $digest

這就是我們的 Scope 的樣子:

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( ) {

};

Scope.prototype.$digest = function( ) {

};

$watch 應該採用兩個引數,wExplistener。我們可以在 Scope 中設定值。

$watch 被呼叫時,我們將它們傳送到先前儲存在 Scope 中的 $$watcher 值中。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {

};

在這裡,我們會注意到 listener 設定為一個空函式。

當沒有提供 listener 時,你可以按照此示例輕鬆為所有變數註冊 $watch

現在我們將執行 $digest。我們可以檢查舊值和新值是否相等。

如果監聽器尚未被觸發,我們也可以觸發它。為此,我們將迴圈直到它們相等。

dirtyChecking 變數幫助我們實現了這個目標,無論值是否相等。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirtyChecking;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};

現在,我們將設計一個新的範圍示例。我們將把它分配給 $scope;然後,我們將註冊一個 watch 函式。

然後我們將更新它並消化它。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirtyChecking;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};


var $scope = new Scope();

$scope.newName = 'Subhan';

$scope.$watch(function(){
    return $scope.newName;
}, function( newVal, oldVal ) {
    console.log(newVal, oldVal);
} );

$scope.$digest();

我們已經成功實現了髒資料檢查。當我們觀看控制檯時,你將觀察其日誌。

#AngularJs
Subhan undefined

以前,未定義 $scope.newName。我們已經為 Subhan 修復了它。

我們將把 $digest 函式連線到輸入上的 keyup 事件。通過這樣做,我們不必自己指定它。

這意味著我們也可以實現雙向資料繫結。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirty;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};


var $scope = new Scope();

$scope.newName = 'Subhan';

var element = document.querySelectorAll('input');

element[0].onkeyup = function() {
    $scope.newName = element[0].value;

    $scope.$digest();
};

$scope.$watch(function(){
    return $scope.newName;
}, function( newVal, oldVal ) {
    console.log('Value updated In Input - The new value is ' + newVal);

    element[0].value = $scope.newName;
} );

var updateScopeValue = function updateScopeValue( ) {
    $scope.name = 'Butt';
    $scope.$digest();
};

使用這種技術,我們可以輕鬆地更新輸入的值,如 $scope.newName 中所示。你也可以呼叫 updateScopeValue,輸入的值會顯示出來。

Rana Hasnain Khan avatar Rana Hasnain Khan avatar

Rana is a computer science graduate passionate about helping people to build and diagnose scalable web application problems and problems developers face across the full-stack.

LinkedIn