数据的双向绑定可能是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件事:

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, '', nullNaN之一时返回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-validng-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件事:

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的调用者定义的。

最后还是画一幅图总结一下: