Dirty Checking en AngularJS

Rana Hasnain Khan 31 mayo 2022
Dirty Checking en AngularJS

Presentaremos cómo realizar comprobaciones sucias en AngularJs.

Implementar comprobación sucia en AngularJS

AngularJS realiza comprobaciones sucias en las variables $scope para el enlace de datos bidireccional. Ember.js realiza un enlace de datos bidireccional al arreglar mediante programación los setters y getters, pero en Angular, la verificación sucia le permite buscar variables que podrían estar disponibles o no.

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

La función $scope.$watch se usa cuando queremos ver cuándo se altera una variable. Tenemos que elegir tres argumentos para aplicar esta función: wExp es lo que queremos ver, listener es lo que queremos que haga cuando se actualice y si queremos comprobar una variable o un objeto.

Podemos omitir esto mientras usamos esta función cuando queremos verificar una variable. Veamos un ejemplo como se muestra a continuación.

#AngularJs
$scope.newName = 'Subhan';

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

Angular guardará su función de observador en el $ alcance. Puede comprobar esto iniciando sesión en $scope en la consola.

En $watch, también podemos utilizar una cadena en lugar de una función. Esto también funcionará igual que una función.

Cuando agregamos una cadena en el código fuente de Angular, el código será:

#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);
  };
}

Al aplicar este código, wExp se establecerá como una función. Esto designará a nuestro oyente con la variable con nuestro nombre elegido.

la función $watchers en AngularJS

Todos los observadores seleccionados se almacenarán en la variable $$watchers en $scope. Encontrará una variedad de objetos cuando marquemos $$watchers, como se muestra a continuación.

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

La función unRegWatch es devuelta por la función $watch. Esto muestra que si queremos asignar el $scope.$watch inicial a una variable, podemos lograrlo fácilmente ordenándole que deje de mirar.

Simplemente confirme que hemos abierto y mirado el primer $scope registrado antes de desconectar el observador.

la función $scope.$apply en AngularJS

Cuando intentamos ejecutar controlador/directiva/etc., Angular ejecuta una función dentro de la cual llamamos $scope.$watch. Antes de administrar la función $digest en rootScope, la función $apply ejecutará una función que hayamos elegido.

La función Angular $apply es la siguiente:

#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;
      }
    }
}

el argumento expr en AngularJS

Cuando usamos $scope.$apply, a veces Angular o pasaríamos a través de una función, el argumento expr es esa función.

Mientras usamos esta función, no encontraremos la necesidad de usar $apply con más frecuencia. Veamos qué sucede cuando ng-keydown usa $scope.$apply.

Para enviar la directiva, usamos el siguiente código.

#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});
              });
            });
          };
        }
      };
    }];
  }
);

Este código recorrerá los tipos contrastantes de eventos que se pueden activar, generando una nueva directiva llamada ng(eventName). Cuando se trata de la función de compilación de la directiva, se registra un controlador de eventos en el elemento, donde el evento contiene el nombre de la directiva.

Cuando se activa este evento, Angular ejecutará $scope.$apply, ofreciéndole una función para ejecutar.

Así es como el valor $scope se actualizará con el valor del elemento, pero este enlace es solo un enlace unidireccional. La razón de esto es que hemos aplicado ng-keydown, que nos permite cambiar solo cuando se aplica este evento.

¡Como resultado, se obtiene un nuevo valor!

Nuestro objetivo principal es lograr un enlace de datos bidireccional. Para lograr este objetivo, podemos utilizar el ng-model.

Para que ng-model funcione, utiliza tanto $scope.$watch como $scope.$apply .

La entrada que hemos dado se unirá al controlador de eventos por ng-model. En este punto, ¡se llama $scope.$watch!

$scope.$watch se llama en el controlador de la directiva.

#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;
});

Cuando se usa solo un argumento al configurar $scope.$watch, la función que hemos elegido se aplicará aunque esté actualizada o no. La función presente en el ng-model examina si el modelo y la vista están sincronizados.

Si no está sincronizado, la función le dará al modelo un nuevo valor y lo actualizará. Esta función nos permite saber cuál es el nuevo valor devolviendo el nuevo valor cuando ejecutamos esta función en $digest.

Por qué no se dispara el Listener

Volvamos al punto del ejemplo en el que hemos desinscrito la función $scope.$watch en la función similar a la que hemos descrito. Ahora podemos reconocer el motivo por el que no se nos notifica sobre la actualización de $scope.name incluso cuando lo hayamos actualizado.

Como sabemos, Angular ejecuta $scope.$apply en cada función de controlador de directiva. La función $scope.$apply ejecutará $digest solo con una condición cuando hayamos estimado la función de controlador de la directiva.

Esto significa que hemos cancelado la inscripción de la función $scope.$watch antes de que pudiera funcionar, por lo que hay una posibilidad mínima o nula de que se la llame.

la función $digest en AngularJS

Puede llamar a esta función en $rootScope mediante $scope.$apply. Podemos ejecutar el bucle de resumen en $rootScope, y luego pasará por encima de los ámbitos y en su lugar ejecutará el bucle de resumen.

el bucle de resumen activará todas nuestras funciones wExp en la variable $$watchers. Los comparará con el último valor conocido.

Si los resultados son negativos, el oyente será despedido.

En el bucle de resumen, cuando se ejecuta, se repite dos veces. Una vez, recorrerá los observadores y luego volverá a repetirse hasta que el bucle ya no esté sucio.

Cuando wExp y el último valor conocido no son equivalentes, decimos que el bucle está sucio. Por lo general, este ciclo se ejecutará una vez y generará un error si se ejecuta más de diez veces.

Angular puede ejecutar $scope.$apply en cualquier cosa que tenga la posibilidad de cambiar el valor de un modelo.

Tienes que ejecutar $scope.$apply(); cuando hemos actualizado $scope fuera de Angular. Esto notificará a Angular que el alcance se ha actualizado.

Diseñemos una versión básica de verificación sucia.

Todos los datos que queramos guardar estarán en la función Scope. Expandiremos el objeto en una función para duplicar $digest y $watch.

Debido a que no necesitamos evaluar ninguna función en relación con Scope, no usaremos $apply. Usaremos directamente $digest.

Así es como se verá nuestro Scope:

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

Scope.prototype.$watch = function( ) {

};

Scope.prototype.$digest = function( ) {

};

Hay dos parámetros, wExp y oyente, que $watch debe adoptar. Podemos establecer los valores en Scope.

Cuando se llama a $watch, los enviamos al valor $$watcher previamente almacenado en Scope.

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

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

Scope.prototype.$digest = function( ) {

};

Aquí notaremos que listener se establece en una función vacía.

Puede registrar fácilmente un $watch para todas las variables cuando no se ofrece ningún listener siguiendo este ejemplo.

Ahora vamos a operar $digest. Podemos inspeccionar si el valor anterior y el valor nuevo son equivalentes.

También podemos despedir al oyente si aún no lo está. Para lograr esto, haremos un bucle hasta que sean equivalentes.

La variable dirtyChecking nos ha ayudado a conseguir este objetivo, independientemente de que los valores sean iguales.

#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);
};

Ahora, diseñaremos un nuevo ejemplo de nuestro alcance. Asignaremos esto a $scope; luego, registraremos una función de reloj.

Luego lo actualizaremos y lo digeriremos.

#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();

Hemos implementado con éxito la verificación sucia. Observará sus registros cuando observemos la consola.

#AngularJs
Subhan undefined

Anteriormente, $scope.newName no estaba definido. Lo hemos arreglado para Subhan.

Conectaremos la función $digest a un evento keyup en una entrada. Al hacer esto, no tenemos que designarlo nosotros mismos.

Esto significa que también podemos lograr un enlace de datos bidireccional.

#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();
};

Con esta técnica, podemos actualizar fácilmente el valor de la entrada, como se ve en $scope.newName. También puede llamar a updateScopeValue, y el valor de entrada lo mostrará.

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