Explaining the order of the ngModel pipeline, parsers, formatters, viewChangeListeners, and $watchers

前端 未结 1 1794
悲&欢浪女
悲&欢浪女 2021-01-04 13:15

It\'s not easy to frame this question, so I will try to explain what I want to know with an example:

Consider this simple angularjs app: PLU

相关标签:
1条回答
  • 2021-01-04 13:43

    To summarize the problem, ngModelController has a process to go through before watches will be fired. You're logging the outer $scope property before ngModelController has processed the change and caused a $digest cycle, which would in turn fire $watchers. I wouldn't consider the model updated until that point.

    This is a complex system. I made this demo as a reference. I recommend changing the return values, typing, and clicking - just messing around with it in all kinds of ways and checking the log. This makes it clear very quickly how everything works.

    Demo (have fun!)

    ngModelController has it's own arrays of functions to run as responses to different changes.

    ngModelController has two kinds of "pipelines" for determining what to do with a kind of change. These allow the developer to control the flow of values.

    If the scope property assigned as ngModel changes, the $formatter pipeline will run. This pipeline is used to determine how the value coming from $scope should be displayed in the view, but leaves the model alone. So, ng-model="foo" and $scope.foo = '123', would typically display 123 in the input, but the formatter could return 1-2-3 or any value. $scope.foo is still 123, but it is displayed as whatever the formatter returned.

    $parsers deal with the same thing, but in reverse. When the user types something, the $parser pipeline is run. Whatever a $parser returns is what will be set to ngModel.$modelValue. So, if the user types abc and the $parser returns a-b-c, then the view won't change, but $scope.foo now is a-b-c.

    After either a $formatter or $parser runs, $validators will be run. The validity of whatever property name is used for the validator will be set by the return value of the validation function (true or false).

    $viewChangeListeners are fired after view changes, not model changes. This one is especially confusing because we're referring to $scope.foo and NOT ngModel.$modelValue. A view will inevitably update ngModel.$modelValue (unless prevented in the pipeline), but that is not the model change we're referring to. Basically, $viewChangeListeners are fired after $parsers and NOT after $formatters. So, when the view value changes (user types), $parsers, $validators, then $viewChangeListeners. Fun times =D

    All of this happens internally from ngModelController. During the process, the ngModel object is not updated like you might expect. The pipeline is passing around values that will affect that object. At the end of the process, the ngModel object will be updated with the proper $viewValue and $modelValue.

    Finally, the ngModelController is done and a $digest cycle will occur to allow the rest of the application to respond to the resulting changes.

    Here's the code from the demo in case anything should happen to it:

    <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;
    })
    
    ;
    
    0 讨论(0)
提交回复
热议问题