解释 ngModel 管道、解析器、格式化程序、viewChangeListeners 和 $watchers 的顺序 [英] Explaining the order of the ngModel pipeline, parsers, formatters, viewChangeListeners, and $watchers

查看:20
本文介绍了解释 ngModel 管道、解析器、格式化程序、viewChangeListeners 和 $watchers 的顺序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

提出这个问题并不容易,所以我会试着用一个例子来解释我想知道的:

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

考虑这个简单的 angularjs app:PLUNKER

Consider this simple 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 中,我们正在实例化mainCtrl"内的testDirective",并将mainCtrl"范围的isChecked"属性与指令隔离范围的isChecked"属性绑定.
  • 该指令呈现一个复选框,该复选框具有isChecked"范围属性作为模型.
  • 当我们选中或取消选中复选框时,我们可以看到两个范围的两个属性同时更新.

到目前为止,一切都很好.

So far, so good.

现在让我们做一点改变,像这样:PLUNKER

Now let's make a little change, like this: 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>

我们所做的唯一一件事是:

The only thing that we have done is:

  • 在控制器范围内定义一个函数,该函数执行 window.alert 指示控制器范围的isChecked"属性是选中还是未选中.(我故意执行 window.alert,因为我希望停止执行)
  • 将该函数绑定到指令中
  • 在指令复选框的ng-change"中触发该函数.
  • Define a function in the scope of the controller that does a window.alert indicating if the 'isChecked' attribute of the controller's scope is checked or unchecked. (I'm doing a window.alert on purpose, because I want the execution to stop)
  • Bind that function into the directive
  • In the "ng-change" of the checkbox of the directive trigger that function.

现在,当我们选中或取消选中复选框时,我们会收到一个警报,在该警报中,我们可以看到指令的范围尚未更新.好的,所以有人会认为 ng-change 在模型更新之前被触发,而且在显示警报时,我们可以看到根据浏览器中呈现的文本isChecked"具有两个范围内的值相同.好吧,没什么大不了的,如果这就是ng-change"的行为方式,那么就这样吧,我们总是可以设置一个 $watch 并在那里运行该函数......但是让我们做另一个实验:

Now, when we check or uncheck the checkbox we get an alert, and in that alert we can see that the scope of the directive hasn't been updated yet. Ok, so one would think that the ng-change gets triggered before the model gets updated, also while the alert is being displayed we can see that according to the text rendered in the browser "isChecked" has the same value in both scopes. All right, no big deal, if that's how the "ng-change" behave, so be it, we can always set a $watch and run the function there... But lets do another experiment:

像这样: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>'
    };
}); 

现在我们只是使用指令作用域的函数来做和控制器作用域的函数一样的事情,但是这次结果是模型已经更新了,所以看起来此时指令的范围已更新,但控制器的范围未更新......奇怪!

Now we are just using a function of the scope of the directive to do the same thing that the function of the scope of the controller was doing, but this time it turns out that the model has been updated, so it seems that at this point the scope of the directive is updated but the scope of the controller is not updated... Weird!

让我们确保是这样: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,而不是在此之后,即指令范围的绑定得到更新.

This time we are using the function of the scope of the directive to trigger the bound function of the controller, and we are passing an argument to the controller's function with the value of the directive's scope. Now in the controller's function we can confirm what we already suspected in the previous step, which is: that the isolated scope gets updated first, then the ng-change gets triggered, and it's not after that, that the bindings of the directive's scope get updated.

现在,最后,我的问题:

Now, finally, my question/s:

  • angularjs 不应该在做其他事情之前同时更新所有绑定的属性吗?
  • 谁能给我详细解释内部发生的事情以证明这种行为是正确的?

换句话说:如果ng-change"在模型更新之前被触发,我可以理解,但我很难理解在更新模型之后和完成之前触发了一个函数填充绑定属性的变化.

In other words: if the "ng-change" got triggered before the model gets updated, I could understand that, but I'm having a very hard time understanding that a function gets triggered after updating the model and before finishing to populate the changes of the bound properties.

如果您读到这里:恭喜并感谢您的耐心等待!

If you read this far: congratulations and thanks for your patience!

约瑟夫

推荐答案

总结这个问题,ngModelControllerwatches 被触发之前有一个过程.您在 ngModelController 处理更改并导致 $digest 循环之前记录外部 $scope 属性,这反过来会触发 $watchers.在那之前,我不会考虑更新 model.

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.

这是一个复杂的系统.我做了这个演示作为参考.我建议更改 return 值、键入和单击 - 只需以各种方式处理它并检查日志.这可以让您很快清楚地了解一切是如何运作的.

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.

ngModelController 有自己的函数数组,可以作为对不同更改的响应运行.

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

ngModelController 有两种管道",用于确定如何处理某种更改.这些允许开发人员控制值的流动.

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.

如果分配为 ​​ngModel 的范围属性发生变化,$formatter 管道将运行.此管道用于确定来自 $scope 的值应如何显示在视图中,但不影响模型.因此,ng-model="foo"$scope.foo = '123' 通常会在输入中显示 123,但是格式化程序可以返回 1-2-3 或任何值.$scope.foo 仍然是 123,但它显示为格式化程序返回的任何内容.

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 处理同样的事情,但相反.当用户输入内容时,$parser 管道就会运行.无论 $parser 返回什么,都会被设置为 ngModel.$modelValue.所以,如果用户输入 abc 并且 $parser 返回 abc,那么视图不会改变,但是 $scope.foo 现在是 abc.

$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.

$formatter$parser 运行后,将运行 $validators.用于验证器的任何属性名称的有效性将由验证函数的返回值(truefalse)设置.

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 在视图更改后触发,而不是模型更改.这个特别令人困惑,因为我们指的是 $scope.foo 而不是 ngModel.$modelValue.视图将不可避免地更新 ngModel.$modelValue(除非在管道中被阻止),但这不是我们所指的 model change.基本上,$viewChangeListeners$parsers 之后触发,而不是在 $formatters 之后触发.因此,当视图值更改(用户类型)时,$parsers、$validators,然后是 $viewChangeListeners.欢乐时光 =D

$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

所有这些都发生在 ngModelController 内部.在此过程中,ngModel 对象不会像您预期的那样更新.管道正在传递将影响该对象的值.在该过程结束时,ngModel 对象将使用正确的 $viewValue$modelValue 进行更新.

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.

最后,ngModelController 完成,$digest 循环将发生,以允许应用程序的其余部分响应产生的更改.

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

;

这篇关于解释 ngModel 管道、解析器、格式化程序、viewChangeListeners 和 $watchers 的顺序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆