从一段.html代码说起谈谈AngularJs中的双向数据绑定

<!DOCTYPE html>

<html lang="en" ng-app>

<--省略head部分代码—>

<body>

input1: <input type="text" ng-model='message'>

input2:<input type="text" ng-model='message'>

view:<div>{{message}}</div>

<script src='bower_components/angular/angular.js'></script>

</body>

</html>

AngularJs神奇的地方就是,其竟然不用自己写JS文件就可以实现简单的双向数据绑定,简单说下双向数据绑定,AngularJs中的双向数据绑定就是视图和模型数据的一个绑定的过程,当视图发生变化时,模型也会相应的更新(可能不同步,下面会说到),当模型数据发生变化时,视图内容也会发生变化(模型数据可能被格式化再显示在视图上),上面的过程就是一个双向数据绑定。而上面的html代码就实现了双向数据绑定,下面将在源码层面分析AngularJs实现双向数据绑定的原理。

本篇文章将从一个AngularJs应用(上面那段简单代码)的运行三个过程来介绍双向数据绑定原理。这三个阶段分别是AngularJS应用的启动、指令编译阶段、应用运行时。而双向数据绑定主要集中在后面两个阶段,因此后面两个阶段将详细解释。

1、AngularJS应用的启动

在AngularJS源码的最后,有这样一段代码:

jqLite(document).ready(function() {

angularInit(document, bootstrap);

});

在version1.4.7版本,的28898行,通过sqlite (AngularJs内置的一个轻量级的jQuery,如果文件中应用了jQuery,则内置的sqlite被替换为jQuery)监听document的DOMcontentLoaded事件,当该事件发生时,trigger事件监听函数,也就是angularInit函数,把bootstrap函数传入,初始化AngularJS应用,当然在这步会在html文件中找ng-app(ng:app 等)指令,当找到该指令AngularJs才会自动启动,否者则需要手动调用bootstrap方法启动AngularJS应用,在bootstrap函数中,该函数主要作用就是创建一个injector然后注入如下服务:

'$rootScope', '$rootElement', '$compile', '$injector'

最后返回injector。

2、编译阶段

当AngularJs应用启动后,$compile服务就会接管创建好的$rootScope。然后收集DOM树中的所有指令,进行编译。好吧,现在我们来找茬:

上面那段代码总共有几个指令?3个?

其实上面那段代码中总共有7个指令,两个input指令,另个ngModel指令,一个ngApp指令,一个script指令还有一个{{}}数据绑定的指令(也可以写成ng-bind)。

script指令主要是引用外部AngularJs文件,这儿和原来的script元素没有太大区别,ngApp指令上面也提到了,启动AngularJs应用用的,当然如果ngApp等号后面有表达式的时候,会启动特定的module,为了把焦点放在数据绑定上,这儿也简化了。那么在上面的例子中和数据绑定有关的指令也就剩下三个了,input、ngModel、{{}}。

input指令

这儿首先提一下,内置指令都是在publishExternalAPI(angular)被暴露出来的,而内置指令也都是通过module.directive()方法生成的。

input指令是通过inputDirective这个指令配制数组配制,代码如下:

var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',

function($browser, $sniffer, $filter, $parse) {

return {

restrict: 'E',

require: ['?ngModel'],

link: {

pre: function(scope, element, attr, ctrls) {

if (ctrls[0]) {

(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0],

$sniffer,

$browser, $filter, $parse);

}

}

}

};

}];

在定义该指令时,首先注入一些服务,'$browser', '$sniffer', '$filter', '$parse',$browser服务是一个AngularJs内置的私有服务,没有发布,主要是用来把window object上面的一些属性或方法隐藏起来,而把这些方法抽象到$browser这个服务中,(具体原理还没有看)$sniffer是AngularJs内置的私有的服务,用来对浏览器进行特性检测用的。

该指令还依赖了ngModel指令,我们上面的input.type是text,因此在link对象中,调用了inputType.text方法来继续配制input指令。而inputType.text方法主要依赖了baseInputType。该方法的主要作用有两个:

1)重写ngModelController上面的$render()方法。

2)绑定keyup、change、input事件,当发生这些事件的时候,更新$viewValue。

首先我们看看$render方法

ctrl.$render = function() {

// Workaround for Firefox validation #12102.

var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;

if (element.val() !== value) {

element.val(value);

}

};

从方法名就可以看出,该方法是用来渲染视图用的,用来把input的value值设置成为noModelController的$viewValue值。当然该方法还做了一些判断,如果$viewValue是undefined、null、NaN,""。这element的value值就更新为空字符串,如果element的value值没有发生变化,也就不用更新了。

再次我们说说input指令上面的绑定的事件,通过$sniffer服务检测浏览器是否支持input事件,若果支持input事件,我们就直接在该事件绑定事件监听函数,通过事件监听函数来更新$viewValue值。(顺便说说input事件,这是一个DOM3事件,HTML5新增,相对于change事件的优点是,只要有输入,就会监听到,而不是change事件那样,input输入框失去焦点才会检测到。input事件可以理解成input中的keyup事件,其还有一个优点是其支持contenteditable属性所在输入元素。其缺点就是浏览器支持情况不统一)。当浏览器不支持input事件时,AngularJs进行优雅降级,通过keyup、change、paste、 cut等事件来进行监听input输入框的输入,其作用都是一样的,通过element的value值来更行$viewValue。

可以总结一下input指令了,其作用就是在element.value和ngModelController之间建立一个连接。

element.value <—> ngModelController.$viewValue

那么问题来了?当我们在input输入框中输入内容的时候,更新了$viewValue,但是$modelValue是怎么更新的呢?又是什么时候更新的呢?更新成为什么值呢?$modelValue就是怎么更新scope上面的数据模型呢?带着上面这些问题我们继续看ngModel指令。

ngModel指令

一切问题的根源都可以从源码来解决。ngModel指令也是在publishExternalAPI(angular)方法中发布的,该指令通过ngModelDirective来配置,而ngModelDirective有依赖于ngModelController控制器,所以我们先说说ngModelController把,它是这篇文章的主角。

1)ngModelController 该控制器在源码中的代码量大概在一千行左右(包括注释)因此不可能完全贴出来。那我们就根据ngModelController暴露出来的方法和属性来叙述吧。顺便把上面提到问题一一解决。

ngModelController上面的方法和属性有如下:

this.$viewValue = Number.NaN;

this.$modelValue = Number.NaN;

this.$$rawModelValue = undefined;

this.$validators = {};

this.$asyncValidators = {};

this.$parsers = [];

this.$formatters = [];

this.$viewChangeListeners = [];

this.$untouched = true;

this.$touched = false;

this.$pristine = true;

this.$dirty = false;

this.$valid = true;

this.$invalid = false;

this.$error = {}; // keep invalid keys here

this.$$success = {}; // keep valid keys here

this.$pending = undefined; // keep pending keys here

this.$name = $interpolate($attr.name || '', false)($scope);

this.$render = loop;

上面是一个ngModel的初始赋值。

this.$viewValue

可以理解成为input元素的视图值,也就是我们看到input输入框中的值,(当然它有时候也会欺骗我们,就是我们看到输入框中的值不等于$viewValue,这个时候我们就需要手动调用this.$render方法,这种情况通常是在我们手动条用$setViewValue方法设置$viewValue值的时候,input输入框中的内容不会实时更新。需要手动调用$render方法)。$viewValue可以通过$setViewValue方法来手动设置,或者在input指令中通过事件绑定,在input输入框中输入内容是,$viewValue会自动更新。

this.$modelValue

通过这个名字也就明白其含义了,表示数据模型值,也就是最后赋值到scope对象上面的值。当$viewValue更新时,this.$modelValue有可能也同步或异步更新。这句话比较拗口,注意其中三个单词「有可能」「同步」「异步」。

有可能:为什么说有可更新呢?当我们在input元素上面设置ng-model-options指令后,可以导致$modelValue延迟更新。比如:

<input ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }">

上面ng-model-options指令就会导致在没有失去焦点的时候,ngModelOption不是简单的数据绑定例子中有的指令,这儿也就这么提一下就好了。也就是在500ms后,没有调用$rollbackViewValue方法时,数据模型才会更新,这个时候ctrl.$$lastCommittedViewValue(内部属性)也会更新成$viewValue值。所以说,前面为什么说有可能嘛。

同步:当没有设置ng-model-options,数据模型和视图模型就是同步更新的。

异步:当设置了ng-model-options指令,视图模型和数据模型就会异步更新。

this.$$rawModelValue

英文中raw是原材料、未加工的意思,看单词学属性,$$rawModelValue就是没有被加工过的$modelValue.在angularJS中,「$$」表示内部的未发布属性或方法。在AngularJs源码中,有两个地方会对这个属性进行设置,一个是在this.$$parseAndValidate方法中,一个是在给input输入框注册catcher的时候。

this.$$parseAndValidate
 
this.$$parseAndValidate = function() {
var viewValue = ctrl.$$lastCommittedViewValue;
var modelValue = viewValue;
parserValid = isUndefined(modelValue) ? undefined : true;
if (parserValid) {
  for (var i = 0; i < ctrl.$parsers.length; i++) {
    modelValue = ctrl.$parsers[i](modelValue);
    if (isUndefined(modelValue)) {
      parserValid = false;
      break;
    }
  }
}
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
  ctrl.$modelValue = ngModelGet($scope);
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
ctrl.$$rawModelValue = modelValue;
if (allowInvalid) {
  ctrl.$modelValue = modelValue;
  writeToModelIfNeeded();
}
ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
  if (!allowInvalid) {
    ctrl.$modelValue = allValid ? modelValue : undefined;
    writeToModelIfNeeded();
  }
});

上面是 this.$$parseAndValidate 的源码,该函数主要做了两件事,正如其名,第一件事就是parse,第二件事就是validate。

this.$parsers && this.$validators

该属性是一个函数列表组成的数组,pipeline。主要是在从$viewValue —> $modelValue的过程中,用来处理$viewValue数据,在this.$$parseAndValidate函数执行的过程中,$parsers中的函数会被一个接一个的调用,上一个函数的返回值,将作为下一个函数的参数。如果$parsers列表中的所有函数都通过后,就把返回值赋值给$$rawModelValue. 然后再运行$validators collection中的验证函数,类似于$parsers执行过程,$validators collection中的函数也会一个接一个调用,首先传入$$rawModelValue值,最后返回的值如果不是undefined,则把该返回值写入数据模型中,也就是scope对象上面的属性中。

this.$viewChangeListeners = [];

当$parsers 和$validators这两个pipeline都执行过后,$modelValue发生变化,这时候,$viewChangeListeners中的监听函数就会一个接一个被执行。

this.$formatters = [];

该数组也是一个函数列表,顾名思义,就知道这个函数列表是一个格式化模型数据的函数组。这个函数数组主要是在 $modelValue —> $viewValue 的过程中执行。

在ngmodelController还有最重要的一点,对数据的$modelValue数据的监控,发生变化,及时更新$viewValue.在AngularJs中通过如下代码完成:

 
$scope.$watch(function ngModelWatch() {
var modelValue = ngModelGet($scope);
// if scope model value and ngModel value are out of sync
// TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue &&
   // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
   (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
) {
  ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
  parserValid = undefined;
  var formatters = ctrl.$formatters,
      idx = formatters.length;
  var viewValue = modelValue;
  while (idx--) {
    viewValue = formatters[idx](viewValue);
  }
  if (ctrl.$viewValue !== viewValue) {
    ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
    ctrl.$render();
    ctrl.$$runValidators(modelValue, viewValue, noop);
  }
}
return modelValue;
});

在scope的$$watchers中增加这个watcher,也就使得了,当我们数据模型发生变化,$viewValue, 也能够随之而更新。

首先我们从$scope对象中获取modelValue,判断其是否发生了变化,如果没有变化直接返回,如果有变化,就把给新的modelValue赋值给ctrl.$modelValue,并且通过$formatters数组pipeline,把最后返回的值赋值给ctrl.$viewValue。还有最后一步,就是调用$render方法,渲染视图。

上面描述的就完成了下面整个过程:

input.value <<—>>ctrl.$viewValue <<—>>ctrl.$modelValue <<—>> $scope.property

说是双向数据绑定,其实涉及到了上面四个存储数据的变量或属性。

2)ngModelDirective

ngModelDirective主要是用来生成ngModel指令的,其中引用了ngModelController控制器,添加了blur事件,监听了$destroy事件,设置了options。因此ngModelDirective最主要的还是调用了ngModelcontroller控制器,通过该控制器向ngModel暴露了一组很有用的API。

应用运行时

其实这部分就是模拟用户输入到最后视图改变的一整个过程,把第二部分了解的函数,属性,方法串起来。应用运行数据绑定也就是跑一遍下面这个过程:

input.value <<—>>ctrl.$viewValue <<—>>ctrl.$modelValue <<—>> $scope.property

1、在$rootScope上面添加一个message属性,作为绑定的数据模型

在ngModel指令编译的过程中,读取到ng-model这个指令,并且读到后面有个'message'表达式,首先会寻找current scope,在本例中,没有定义控制器,因此,AngularJs上下文中只有一个 $rootScope,然后需找$rootScope 中是否有一个message属性数据。发现没有,因为我压根就没写js代码。这个时候AngularJs就为我们在$rootScope上面添加了一个message属性。

2、注册watcher

在上面的例子中,总共需要注册3个watcher到$rootScope的$$watchers列表中。前两个很好找到,就是对两个input中的数据模型进行监控的$watch函数,在ngModelController中已经提到过,这个过程发生在指令编译的阶段。有了这个watcher,执行$digest方法,当数据模型发生变化后,视图也会相应的更新,渲染。这儿提一点,input上面的$watch函数没有listener,也就是监听器。大家可以思考下问什么不需要listener了。

$rootScope.$$watchers.length 为3 。剩下的一个watcher在那呢?好吧,这儿就要提到「{{}}」该数据绑定指令了,该指令也会在编译的阶段完成编译。该指令具体的源代码我还没有看,但是有一点是肯定的,在编译的过程中,也增加了一个watcher函数,当{{}}中的数据模型发生变化时,及时更新视图数据,然后渲染。

3、用户输入

还记得上面的问题吧:

当我们在input输入框中输入内容的时候,怎么更新了$viewValue?$modelValue又是怎么更新的呢?又是什么时候更新的呢?更新成为什么值呢?$modelValue就是怎么更新scope上面的数据模型呢?

现在我们来模拟用户输入,跟踪数据流动的整个过程。

在文章前面一开始的例子。我们实现的功能就是,当我们在任意一个input元素中输入内容的时候,input输入框,{{}}中的内容都会同步改变。在这个用户交互的过程中发生了以下过程:

input.value <<—>>ctrl.$viewValue <<—>>ctrl.$modelValue <<—>> $scope.property

input.value —>>ctrl.$viewValue —>>ctrl.$modelValue —>> $scope.property—>>{{message}}

第一个过程是一个双向数据流的过程,input框内容发生变化,数据模型相应发生变化,数据模型变化,又会反映到input输入框的变化。比如当我们在input1中输入内容的时候,会改变$rootscope上面的message数据模型值,然后该数据模型值又会同步到input2value上面。同时还有第二个过程,当我们的$rooscope.messaage发生变化时,{{message}}中的视图也会相应发生改变,这个数据流的过程是单向的。因为women没法手动改变{{message}}内容(没法输入)。了解了整个过程后,我们开始回答问题:

1)怎么更新$viewValue?

这个更新得益于input指令,正如前面我们提到过,input指令上面添加了一系列的事件监听事件,比如input、change、keyup。当监听到这些事件后,就会调用$setViewValue方法,把input.value值赋值给$viewValue。

 
this.$setViewValue = function(value, trigger) {
ctrl.$viewValue = value;
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
  ctrl.$$debounceViewValueCommit(trigger);
}
};

该方法主要是用来设置$viewValue的值,如果设置了option选项,也就是input上面添加了ng-model-options指令,就会调用ctrl.$$debounceViewValueCommit(trigger);方法,ctrl.$$debounceViewValueCommit(trigger);方法主要是用来延迟执行ctrl.$commitViewValue();方法的,因为本例子中没有添加该指令,这儿就提下就好了。

2)$modelValue又是怎么更新的呢?

这儿要分两种情况,当设置了ng-model-options指令时,数据是执行一条数据流,当没有设置ng-model-options时候,数据又会执行另外一条数据流。先从第一种情况说起:

第一种情况

$viewValue —>> $$lastCommittedViewValue —>$rawModelValue —>>$modelValue — >$rootScope.message

因为设置了ng-model-options,所以$viewValue和$modelValue更新可能就不同步了,而这一延迟执行主要就在$viewValue —> $$lastCommittedViewValue这一步执行,这一步执行的主要函数是$$debounceViewValueCommit。

this.$parsers数组函数主要是在$$lastCommittedViewValue —>$rawModelValue这一步执行,执行的主要函数是this.$$parseAndValidate。

this.$validaters collection是在$rawModelValue —>>$modelValue这一步执行,这一步也是在this.$$parseAndValidate函数中完成的。

最后一步,$modelValue — >$rootScope.message,就是把数据模型的值写入到scope对象上面。

第二种情况

$viewValue —>> $$lastCommittedViewValue —>$rawModelValue —>>$modelValue — >$rootScope.message

本例中没有设置ng-model-options指令,本例也就属于第二种情况。其实过程差不多,只是,$viewValue —>> $$lastCommittedViewValue这一步不再延迟执行。

3)$modelValue更新成为什么值呢?

若果$$rawModelValue的值通过validators collection中的所有验证,则$modelValue的值就是最初的$viewValue值,如果没有通过验证,则该值为undefined。

4)$modelValue就是怎么更新scope上面的数据模型呢?
 
function writeToModelIfNeeded() {
  if (ctrl.$modelValue !== prevModelValue) {
    ctrl.$$writeModelToScope();
  }
}
 
this.$$writeModelToScope = function() {
ngModelSet($scope, ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
  try {
    listener();
  } catch (e) {
    $exceptionHandler(e);
  }
});
 };

$modelValue改变时,通过以上两个函数来修改$scope上面的数据模型。值得一提的是,在

$modelValue —>$scope.message

这一步才调用$viewChangeListeners列表上面的监听函数。

猜你喜欢

转载自blog.csdn.net/magiclr/article/details/49617147