解释 ngModel 管道、解析器、格式化程序、viewChangeListeners 和 $watchers 的顺序 [英] Explaining the order of the ngModel pipeline, parsers, formatters, viewChangeListeners, and $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 awindow.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!
约瑟夫
推荐答案
总结这个问题,ngModelController
在 watches
被触发之前有一个过程.您在 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
.用于验证器的任何属性名称的有效性将由验证函数的返回值(true
或 false
)设置.
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屋!