数据的双向绑定可能是Angular最为人们熟知的特性之一。举个最简单的例子
可以在result页面中看到,每当在input中输入时,$scope中对应model的值也改变了。反之,当用户点击Set按钮在$scope中更新了model的值时,input输入框中的内容也对应更新了。
所有的这些魔法只需要我们在input元素上指定一个ng-model
属性。可见ng-model
这个directive是双向绑定这一特性不可缺少一点。本文就继续从源代码入手看看ngModel的实现方式以及其它directive是如何与ngModel交互的。
ngModel
被定义在input.js。从名字上可看出,该文件还定义input
这个directive,从这点也可以看出ngModel与input之间的紧密关系。
注: 文中所有涉及到的AngularJS源码均来自angular-1.2.8版本。
var ngModelDirective = function() {
return {
require: ['ngModel', '^?form'],
controller: NgModelController,
link: function(scope, element, attr, ctrls) {
// notify others, especially parent forms
var modelCtrl = ctrls[0],
formCtrl = ctrls[1] || nullFormCtrl;
formCtrl.$addControl(modelCtrl);
scope.$on('$destroy', function() {
formCtrl.$removeControl(modelCtrl);
});
}
};
};
可以看出ngModel依赖于ngModelController以及可选的formController。ngModel在link方法中只做了2件事:
- 如果声明了ngModel的元素出现在一个form中,那么就向上层form注册自身
- 注册了
$destroy
事件的监听器,该事件触发时告知上层form移除对自身的引用
link方法如此简单以至于我们还没看到关于数据绑定的任何信息。看来问题的答案都藏在ngModelController中了。接下来看看ngModelController中都发生了什么。
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
function($scope, $exceptionHandler, $attr, $element, $parse) {
this.$viewValue = Number.NaN; // 页面中的值
this.$modelValue = Number.NaN; // $scope中model的值
this.$parsers = []; // 数组,包含了一些列函数用于对数据进行处理,过滤,合法性检查
this.$formatters = []; // 同$parsers,但使用场景不一样
this.$viewChangeListeners = [];
this.$pristine = true; // 数据是否处于原始状态, 即尚未被用户修改
this.$dirty = false; // 数据是否被用户修改过
this.$valid = true; // 数据是否合法
this.$invalid = false;
this.$name = $attr.name;
这里可以注意到所有的属性都定义在了this
上。这样当其他directive通过require ngModel来拿到了ngModelController时就可以通过这些变量来访问ngModel的内部状态了。
var ngModelGet = $parse($attr.ngModel),
ngModelSet = ngModelGet.assign;
if (!ngModelSet) {
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
$attr.ngModel, startingTag($element));
}
通过$parse
服务根据指定的model名字生成getter和setter方法。这里检查用户指定的model名字是否是一个可被赋值的表达式。如果不可被赋值,则会抛出错误。关于什么样的表达式会抛出non-assignable
错误,这里有详细的说明。
this.$render = noop;
$render
属性是一个方法。该方法的作用是当$scope中的model被修改时去更新页面显示。由于ngModel并不知道其调用者需要怎样对数据进行格式化,已经该更新哪个页面元素,所以该方法会被留给ngModel的调用者自行定义。因此这里只是将其设置为一个空函数。
this.$isEmpty = function(value) {
return isUndefined(value) || value === '' || value === null || value !== value;
};
$isEmpty方法用来判断输入值是否为空,默认的$isEmpty会在input为undefined
, ''
, null
或 NaN
之一时返回true。ngModel的调用者可以通过覆写该方法来实现定制的empty判断。
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
invalidCount = 0, // used to easily determine if we are valid
$error = this.$error = {}; // keep invalid keys here
获取对上层form的引用,用于在model值合法性改变时通知上层form。
invalidCount是对当前不合法属性的计数。一个model可能有多个条件来判断其值是否合法。每一个不合法的条件都会让该计数器加一。
$error哈希则保存了具体不合法的属性的名字。
// Setup initial state of the control
$element.addClass(PRISTINE_CLASS);
toggleValidCss(true);
// convenience method for easy toggling of classes
function toggleValidCss(isValid, validationErrorKey) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
$element.
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
}
设置element的初始CSS状态。默认情况下,当一个声明了ngModel的element在页面初始化后会被添加上ng-valid
和ng-pristine
两个类。ng-valid在model值合法时出现,ng-pristine类则表示页面中的值尚未被用户修改,一旦用户修改页面值,ng-pristin类就会从元素上移除。
toggleValidCss方法用来根据model值是否合法来设置相应CSS类。
this.$setValidity = function(validationErrorKey, isValid) {
// Purposeful use of ! here to cast isValid to boolean in case it is undefined
// jshint -W018
if ($error[validationErrorKey] === !isValid) return;
// jshint +W018
if (isValid) {
if ($error[validationErrorKey]) invalidCount--;
if (!invalidCount) {
toggleValidCss(true);
this.$valid = true;
this.$invalid = false;
}
} else {
toggleValidCss(false);
this.$invalid = true;
this.$valid = false;
invalidCount++;
}
$error[validationErrorKey] = !isValid;
toggleValidCss(isValid, validationErrorKey);
parentForm.$setValidity(validationErrorKey, isValid, this);
};
$setValidity方法用来设置model的值是否合法。它会做以下3件事:
- 更新$invalidCount计数器的值
- 根据是否valid更新元素CSS类
- 告知上层form此model的合法性,便于上层form更新form状态。
this.$setPristine = function () {
this.$dirty = false;
this.$pristine = true;
$element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
};
$setPristine方法用来移除dirty状态,设置pristine状态。(暂时没发现在什么情形下会这么做)
this.$setViewValue = function(value) {
this.$viewValue = value;
// change to dirty
if (this.$pristine) {
this.$dirty = true;
this.$pristine = false;
$element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
parentForm.$setDirty();
}
forEach(this.$parsers, function(fn) {
value = fn(value);
});
if (this.$modelValue !== value) {
this.$modelValue = value;
ngModelSet($scope, value);
forEach(this.$viewChangeListeners, function(listener) {
try {
listener();
} catch(e) {
$exceptionHandler(e);
}
});
}
};
该方法用来在页面值被修改时用来更新$scope中的model值,通常被ngModel调用者使用。该方法会: - 移除元素的pristine状态,添加dirty状态 - 逐一调用$parsers数字中的方法处理新值。 - 如果$scope中modle值与经过$parsers处理后的值不同,则更新$scope中的model,同是调用$viewChangeListeners中设置的callbacks
var ctrl = this;
$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);
// if scope model value and ngModel value are out of sync
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;
});
ngModelController定义的最后是一个$scope.$watch调用,它会监视$scope中model值的变化,一旦改变便会调用$formatters中的处理器处理最新值。如果处理过的值与页面值不同,则调用$render方法将这个model的更新同步到页面上去。注意这里的$render是由ngModel的调用者定义的。
最后还是画一幅图总结一下: