Объяснение порядка конвейера ngModel, синтаксических анализаторов, форматеров, viewChangeListeners и $ watchers

этот вопрос нелегко сформулировать, поэтому я попытаюсь объяснить, что я хочу знать, на примере:

рассмотрим этот простой angularjs app: PLUNKER

angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
  $scope.isChecked = false;
})
.directive("testDirective", function () {
    return {
        restrict: 'E',
        scope: {
            isChecked: '='
        },
        template: '<label><input type="checkbox" ng-model="isChecked" /> Is it Checked?</label>'+
                  '<p>In the <b>directive's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>'
    };
}); 

этот html-код:

  <body ng-controller="mainCtrl">
    <test-directive is-checked="isChecked"></test-directive>
    <p>In the <b>controller's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>
  </body>

приложение:

  • имеет один контроллер с именем: "mainCtrl", где мы определили переменную области, называемую"isChecked"
  • она также имеет одну директиву под названием "testDirective "с изолированной областью и свойством привязки под названием"isChecked".
  • и в html мы создаем экземпляр " testDirective "внутри" mainCtrl "и связываем свойство" isChecked "области" mainCtrl "со свойством" isChecked " изолированной области директивы.
  • директива отображает флажок, который имеет свойство "isChecked" scope в качестве модели.
  • когда мы проверяем или снимаем флажок, мы видим, что оба свойства обеих областей обновляются одновременно.

пока все хорошо.

теперь давайте немного изменимся, вот так:PLUNKER

angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
  $scope.isChecked = false;
  $scope.doingSomething = function(){alert("In the controller's scope is " + ($scope.isChecked?"checked!":"not checked"))};
})
.directive("testDirective", function () {
    return {
        restrict: 'E',
        scope: {
            isChecked: '=',
            doSomething: '&'
        },
        template: '<label><input type="checkbox" ng-change="doSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
                  '<p>In the <b>directive's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>'
    };
}); 

и так:

<!DOCTYPE html>
<html ng-app="testApp">
  <head>
    <script data-require="angular.js@1.3.0-beta.5" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>
  <body ng-controller="mainCtrl">
    <test-directive is-checked="isChecked" do-something="doingSomething()"></test-directive>
    <p>In the <b>controller's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>
  </body>
</html>

единственное что мы сделали-это:

  • определите функцию в области контроллера, которая выполняет window.alert указывает, является ли атрибут "isChecked" области контроллера checked или unchecked. (Я делаю window.alert нарочно, потому что я хочу, чтобы исполнение остановилось)
  • свяжите эту функцию в директиву
  • в поле" ng-change " флажка директивы запускает эту функцию.

теперь, когда мы проверяем или снимаем флажок, мы получаем предупреждение, и в этом предупреждении мы видим, что область действия директивы еще не обновлена. Хорошо, так можно подумать, что ng-change сработал до того, как модель получает обновлено, также во время отображения предупреждения мы видим, что в соответствии с текстом, отображаемым в браузере, "isChecked" имеет одинаковое значение в обеих областях. Ладно, ничего страшного, если так ведут себя" ng-change", так и быть, мы всегда можем установить $watch и запустить функцию... Но давайте проведем еще один эксперимент:

такой: PLUNKER

.directive("testDirective", function () {
    return {
        restrict: 'E',
        scope: {
            isChecked: '=',
            doSomething: '&'
        },
        controller: function($scope){
          $scope.internalDoSomething = function(){alert("In the directive's scope is " + ($scope.isChecked?"checked!":"not checked"))};
        },
        template: '<label><input type="checkbox" ng-change="internalDoSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
                  '<p>In the <b>directive's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>'
    };
}); 

теперь мы просто используем функцию области директива делать то же самое, что делала функция области действия контроллера, но на этот раз оказывается, что модель была обновлена, поэтому кажется, что на данный момент область действия директивы обновляется, но область действия контроллера не обновляется... Странно!

давайте убедимся, что это так: PLUNKER

angular.module('testApp', [])
.controller('mainCtrl', function($scope) {
  $scope.isChecked = false;
  $scope.doingSomething = function(directiveIsChecked){
    alert("In the controller's scope is " + ($scope.isChecked?"checked!":"not checked") + "n"
        + "In the directive's scope is " + (directiveIsChecked?"checked!":"not checked") );
  };
})
.directive("testDirective", function () {
    return {
        restrict: 'E',
        scope: {
            isChecked: '=',
            doSomething: '&'
        },
        controller: function($scope){
          $scope.internalDoSomething = function(){ $scope.doSomething({directiveIsChecked:$scope.isChecked}) }; 
        },
        template: '<label><input type="checkbox" ng-change="internalDoSomething()" ng-model="isChecked" /> Is it Checked?</label>'+
                  '<p>In the <b>directive's</b> scope <b>{{isChecked?"it's checked":"it isn't checked"}}</b>.</p>'
    };
}); 

на этот раз мы используем функцию области действия директивы для вызовите связанную функцию контроллера, и мы передаем аргумент функции контроллера со значением области действия директивы. Теперь в функции контроллера мы можем подтвердить то, что мы уже подозревали на предыдущем шаге, а именно: сначала обновляется изолированная область, затем ng-change запускается, и это не после этого, что привязки области директивы обновляются.

теперь, наконец, мой вопрос/s:

  • не стоит angularjs обновляет все связанные свойства одновременно, прежде чем делать что-либо еще?

другими словами: если" ng-change " был запущен до обновления модели, я мог бы это понять, но мне очень сложно понять, что функция запускается после обновления модели и до завершения заполнения изменений связанных свойств.

если Вы читаете это далеко: поздравления и спасибо за ваше терпение!

Хосеп

1 ответов


резюмируя проблему,ngModelController имеет процесс, чтобы пройти до watches будет уволен. Вы регистрируете внешний $scope свойства перед ngModelController обработал изменение и вызвал цикл $ digest, который, в свою очередь, уволил $watchers. Я бы не стал рассматривать model обновлено до этого момента.

это сложная система. Я сделал эту демонстрацию в качестве ссылки. Я рекомендую изменить return значения, ввод и нажатие-просто возиться с ним во всех видах способы и проверка журнала. Это позволяет очень быстро понять, как все работает.

Demo (получайте удовольствие!)

ngModelController имеет собственные массивы функций для запуска в качестве ответов на различные изменения.

ngModelController имеет два вида "трубопроводов" для определения того, что делать с каким-либо изменением. Они позволяют разработчику управлять потоком значений.

если свойство scope назначено как ngModel изменений будет работать. Этот конвейер используется для определения того, как значение исходит из $scope должно отображаться в представлении, но оставляет модель в покое. Итак,ng-model="foo" и $scope.foo = '123', обычно отображается 123 на входе, но форматер может вернуться 1-2-3 или какую-либо ценность. $scope.foo по-прежнему 123, но он отображается как независимо от того, что вернул форматер.

$parsers дело с тем же самым, но в обратном порядке. Когда пользователь вводит что-то, запускается конвейер $ parser. Что $parser returns-это то, что будет установлено в ngModel.$modelValue. Итак, если пользователь вводит abc и $parser возвращает a-b-c, то вид не изменится, но $scope.foo сейчас a-b-c.

после $formatter или $parser работает, $validators будет работать. Действительность любого имени свойства, используемого для валидатора, будет установлена возвращаемым значением функции проверки (true или false).

$viewChangeListeners запускаются после изменения вида, а не изменения модели. Это особенно запутанно, потому что мы имеем в виду $scope.foo, а не ngModel.$modelValue. Представление неизбежно обновится ngModel.$modelValue (если не предотвращено в трубопроводе), но это не model change мы имеем в виду. В основном, $viewChangeListeners уволен после $parsers и не позднее $formatters. Итак, когда изменяется значение представления (типы пользователей),$parsers, $validators, then $viewChangeListeners. Веселые времена =D

все это происходит внутри ngModelController. Во время процесса ngModel объект не обновляется, как вы могли бы ожидать. Конвейер передает значения, которые будут влиять на этот объект. В конце процесса,ngModel объект будет обновлен с помощью правильного $viewValue и $modelValue.

наконец,ngModelController делается и $digest цикл будет происходить, чтобы позволить остальной части приложения, чтобы ответить на связанные с этим изменения.

вот код из демо на случай, если с ним что-то случится:

<form name="form">
  <input type="text" name="foo" ng-model="foo" my-directive>
</form>
<button ng-click="changeModel()">Change Model</button>
<p>$scope.foo = {{foo}}</p>
<p>Valid: {{!form.foo.$error.test}}</p>

JS:

angular.module('myApp', [])

.controller('myCtrl', function($scope) {

  $scope.foo = '123';
  console.log('------ MODEL CHANGED ($scope.foo = "123") ------');

  $scope.changeModel = function() {
    $scope.foo = 'abc';
    console.log('------ MODEL CHANGED ($scope.foo = "abc") ------');
  };

})

.directive('myDirective', function() {
  var directive = {
    require: 'ngModel',
    link: function($scope, $elememt, $attrs, $ngModel) {

      $ngModel.$formatters.unshift(function(modelVal) {
        console.log('-- Formatter --', JSON.stringify({
          modelVal:modelVal,
          ngModel: {
            viewVal: $ngModel.$viewValue,
            modelVal: $ngModel.$modelValue
          }
        }, null, 2))
        return modelVal;
      });

      $ngModel.$validators.test = function(modelVal, viewVal) {
        console.log('-- Validator --', JSON.stringify({
          modelVal:modelVal,
          viewVal:viewVal,
          ngModel: {
            viewVal: $ngModel.$viewValue,
            modelVal: $ngModel.$modelValue
          }
        }, null, 2))
        return true;
      };

      $ngModel.$parsers.unshift(function(inputVal) {
        console.log('------ VIEW VALUE CHANGED (user typed in input)------');
        console.log('-- Parser --', JSON.stringify({
          inputVal:inputVal,
          ngModel: {
            viewVal: $ngModel.$viewValue,
            modelVal: $ngModel.$modelValue
          }
        }, null, 2))
        return inputVal;
      });

      $ngModel.$viewChangeListeners.push(function() {
        console.log('-- viewChangeListener --', JSON.stringify({
          ngModel: {
            viewVal: $ngModel.$viewValue,
            modelVal: $ngModel.$modelValue
          }
        }, null, 2))
      });

      // same as $watch('foo')
      $scope.$watch(function() {
        return $ngModel.$viewValue;
      }, function(newVal) {
        console.log('-- $watch "foo" --', JSON.stringify({
          newVal:newVal,
          ngModel: {
            viewVal: $ngModel.$viewValue,
            modelVal: $ngModel.$modelValue
          }
        }, null, 2))
      });


    }
  };

  return directive;
})

;