前言


  为了后面描述方便,我们将保存模块的对象modules叫做模块缓存。我们跟踪的例子如下

  <div ng-app="myApp" ng-controller='myCtrl'>
<input type="text" ng-model='name'/>
<span style='width: 100px;height: 20px; margin-left: 300px;'>{{name}}</span>
</div>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.name = 1;
});
</script>

  在angular初始化中,在执行完下面代码后

publishExternalAPI(angular);
angular.module("ngLocale", [], ["$provide", function($provide) {...}]);

  模块缓存中保存着有两个模块

modules = {
ng:{
_invokeQueue: [],
_configBlocks:[["$injector","invoke",[["$provide",ngModule($provide)]]]],
_runBlocks: [],
name: "ng",
requires: ["ngLocale"],
...
},
ngLocale: {
_invokeQueue: [],
_configBlocks: [["$injector","invoke",[["$provide", anonymous($provide)]]]],
_runBlocks: [],
name: "ngLocale",
requires: [],
...
}
}

  每个模块都有的下面的方法,为了方便就没有一一列出,只列出了几个关键属性

  animation: funciton(recipeName, factoryFunction),
config: function(),
constant: function(),
controller: function(recipeName, factoryFunction),
decorator: function(recipeName, factoryFunction),
directive: function(recipeName, factoryFunction),
factory: function(recipeName, factoryFunction),
filter: function(recipeName, factoryFunction),
provider: function(recipeName, factoryFunction),
run: function(block),
service: function(recipeName, factoryFunction),
value: function()

  然后执行到我们自己写的添加myApp模块的代码,添加一个叫myApp的模块

modules = {
ng:{ ... },
ngLocale: {... },
myApp: {
_invokeQueue: [],
_configBlocks: [],
_runBlocks: [],
name: "ngLocale",
requires: [],
...
}
}

  执行 app.controller('myCtrl', function($scope) {})的源码中会给该匿名函数添加.$$moduleName属性以确定所属模块,然后往所属模块的_invokeQueue中压入执行代码等待出发执行。

function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
invokeQueue.push([provider, method, arguments]);
return moduleInstance;
};

  然后等到页面加载完成后,bootstrap函数调用中调用了这段代码,传入的参数modules为["ng", ["$provide",function($provide)], "myApp"]

var injector = createInjector(modules, config.strictDi);

  初始化依赖注入对象,里面用到loadModules函数,其中有这段代码

function loadModules(modulesToLoad) {
...
forEach(modulesToLoad, function(module) {
...
function runInvokeQueue(queue) {
var i, ii;
for (i = 0, ii = queue.length; i < ii; i++) {
var invokeArgs = queue[i],
provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
}
} try {
if (isString(module)) {
moduleFn = angularModule(module);
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
runInvokeQueue(moduleFn._invokeQueue);
runInvokeQueue(moduleFn._configBlocks);
}
...
}
});
}

  先前在app.controller('myCtrl', function($scope) {})中向myApp模块的_invokeQueue中添加了等待执行的代码

_invokeQueue = [["$controllerProvider","register",["myCtrl",function($scope)]]]

  现在执行之,最后在下面函数中给当前模块的内部变量controllers上添加一个叫"myCtrl"的函数属性。

  this.register = function(name, constructor) {
assertNotHasOwnProperty(name, 'controller');
if (isObject(name)) {
extend(controllers, name);
} else {
controllers[name] = constructor;
}
};

  

执行bootstrapApply


  执行

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
function bootstrapApply(scope, element, compile, injector) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
}]
);

  执行该段代码之前的instanceCache是

cache = {
$injector: {
annotate: annotate(fn, strictDi, name),
get: getService(serviceName, caller),
has: anonymus(name),
instantiate: instantiate(Type, locals, serviceName),
invoke: invoke(fn, self, locals, serviceName)
}
}

  执行到调用function bootstrapApply(scope, element, compile, injector) {}之前变成了

cache = {
$$AnimateRunner: AnimateRunner(),
$$animateQueue: Object,
$$cookieReader: (),
$$q: Q(resolver),
$$rAF: (fn),
$$sanitizeUri: sanitizeUri(uri, isImage),
$animate: Object,
$browser: Browser,
$cacheFactory: cacheFactory(cacheId, options),
$compile: compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,previousCompileContext),
$controller: (expression, locals, later, ident),
$document: JQLite[1],
$exceptionHandler: (exception, cause),
$filter: (name),
$http: $http(requestConfig),
$httpBackend: (method, url, post, callback, headers, timeout, withCredentials, responseType),
$httpParamSerializer: ngParamSerializer(params),
$injector: Object,
$interpolate: $interpolate(text, mustHaveExpression, trustedContext, allOrNothing),
$log: Object,
$parse: $parse(exp, interceptorFn, expensiveChecks),
$q: Q(resolver),
$rootElement: JQLite[1],
$rootScope: Scope,
$sce: Object,
$sceDelegate: Object,
$sniffer: Object,
$templateCache: Object,
$templateRequest: handleRequestFn(tpl, ignoreRequestError),
$timeout: timeout(fn, delay, invokeApply),
$window: Window
}

  而且获取到了应用的根节点的JQLite对象传入bootstrapApply函数。

compile中调用var compositeLinkFn = compileNodes(...)编译节点,主要迭代编译根节点的后代节点

  childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
!(childNodes = nodeList[i].childNodes) ||
!childNodes.length)
? null
: compileNodes(childNodes,
nodeLinkFn ? (
(nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
&& nodeLinkFn.transclude) : transcludeFn);

  每一级的节点的处理由每一级的linkFns数组保存起来,并在每一级的compositeLinkFn函数中运用,linkFns的结构是[index, nodeLinkFn, childLinkFn]。

  最终返回一个复合的链接函数。  

compile.$$addScopeClass($compileNodes);给应用根节点加上一个"ng-scope"的class

最后compile(element)返回一个函数publicLinkFn(这个函数很多外部变量就是已经编译好的节点),然后将当前上下文环境Scope代入进这个函数。

publicLinkFn函数中给节点以及后代节点添加了各自的缓存;

接下来是进入$rootScope.$digest();执行数据的脏检测和数据的双向绑定。

  

下面一小点是angular保存表达式的方法:

  标签中的表达式被$interpolate函数解析,普通字段和表达式被切分开放在concat中。比如

<span>名称是{{name}}</span>

  解析后的concat为["名称是", ""],而另一个变量expressionPositions保存了表达式在concat的位置(可能有多个),此时expressionPositions为[1],当脏检测成功后进入compute计算最终值的时候循环执行concat[expressionPositions[i]] = values[i];然后将concat内容拼接起来设置到DOM对象的nodeValue。

function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
});

脏检测与数据双向绑定


  我们用$scope表示一个Scope函数的实例。

$scope.$watch( watchExp, listener[, objectEquality]);

  注册一个监听函数(listener)当监听的表达式(watchExp)发生变化的时候执行监听函数。objectEquality是布尔值类型,确定监听的内容是否是一个对象。watchExp可以是字符串和函数。

  我们在前面的例子的控制器中添加一个监听器

      $scope.name = 1;

      $scope.$watch( function( ) {
return $scope.name;
}, function( newValue, oldValue ) {
alert('$scope.name 数据从' + oldValue + "改成了" + newValue);//$scope.name 数据从1改成了1
});

  当前作用域的监听列表是有$scope.$$watchers保存的,比如现在我们当前添加了一个监听器,其结构如下

$scope.$$watchers = [
{
eq: false, //是否需要检测对象相等
fn: function( newValue, oldValue ) {alert('$scope.name 数据从' + oldValue + "改成了" + newValue);}, //监听器函数
last: function initWatchVal(){}, //最新值
exp: function(){return $scope.name;}, //watchExp函数
get: function(){return $scope.name;} //Angular编译后的watchExp函数
}
];

  除了我们手动添加的监听器外,angular会自动添加另外两个监听器($scope.name变化修改其相关表达式的监听器和初始化时从模型到值修正的监听器)。最终有三个监听器。需要注意的是最用运行的时候是从后往前遍历监听器,所以先执行的是手动添加的监听器,最后执行的是数据双向绑定的监听器(//监听input变化修改$scope.name以及其相关联的表达式的监听器)

  $scope.$$watchers = [
{//监听$scope.name变化修改其相关联的表达式的监听器
eq: false,
exp: regularInterceptedExpression(scope, locals, assign, inputs),
fn: watchGroupAction(value, oldValue, scope),
get: expressionInputWatch(scope),
last: initWatchVal()
},
{//从模型到值修正的监听器
eq: false,
exp: ngModelWatch(),
fn: noop(),
get: ngModelWatch(),
last: initWatchVal()
},
{//手动添加的监听$scope.name变化的监听器
eq: false, //是否需要检测对象相等
fn: function( newValue, oldValue ) {alert('$scope.name 数据从' + oldValue + "改成了" + newValue);}, //监听器函数
last: initWatchVal(){}, //最新值
exp: function(){return $scope.name;}, //watchExp函数
get: function(){return $scope.name;} //Angular编译后的watchExp函数
}
]

  第二个监听器有点特殊,他是使用$scope.$watch(function ngModelWatch() {...});监听的,只有表达式而没有监听函数。官方的解释是:函数监听模型到值的转化。我们没有使用正常的监听函数因为要检测以下几点:

  1.作用域值为‘a'

  2.用户给input初始化的值为‘b’

  3.ng-change应当被启动并还原作用域值'a',但是此时作用域值并没有发生改变(所以在应用阶段最后一次脏检测作为ng-change监听事件执行)

  4. 视图应该恢复到'a'

  这个监听器在初始化的时候判断input的值和$scope.name是否相同,不同则用$scope.name替换之。源码如下

  $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.name做了更改都会执行到这个监听器,监听器里面判断当前作用域的值和DOM元素中的值是否相同,如果不同则给视图渲染作用域的值。

  $watch返回一个叫做deregisterWatch的函数,顾名思义,你可以通过这个函数来解除当前的这个监听。

$scope.$apply()

      $apply: function(expr) {
try {
beginPhase('$apply');
try {
return this.$eval(expr);
} finally {
clearPhase();
}
} catch (e) {
$exceptionHandler(e);
} finally {
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
}

  这个函数具体的只有两个作用:执行传递过来的expr(往往是函数);最后执行$rootScope.$digest();用我的理解来说实际就是一个启动脏值检测的。可能还有一个用处就是加了一个正在执行脏值检测的标志,有些地方会判断当前是否在执行脏值检测从而启动异步执行来保障脏值检测先执行完毕。

  $scope.$apply应该在事件触发的时候调用。$scope.$watch虽然保存着有监听队列,但是这些监听队列是如何和DOM事件关联起来的呢?原来在编译节点的时候angular就给不通的节点绑定了不同的事件,比如基本的input标签通过baseInputType来绑定事件

function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
...
if (!$sniffer.android) {
var composing = false; element.on('compositionstart', function(data) {
composing = true;
}); element.on('compositionend', function() {
composing = false;
listener();
});
}
...
if ($sniffer.hasEvent('input')) {
element.on('input', listener);
} else {
...
element.on('keydown', function(event) {...}); if ($sniffer.hasEvent('paste')) {
element.on('paste cut', deferListener);
}
} element.on('change', listener);
 ...
}

 $rootScope.$digest()

  我们发现脏值检测函数$digest始终是在$rootScope中被$scope.$apply所调用。然后向下遍历每一个作用域并在每个作用域上运行循环。所谓的脏值就是值被更改了。当$digest遍历到某一个作用域的时候,检测该作用域下$$watchers中的监听事件,遍历之并对比新增是否是脏值,如果是则触发对应的监听事件。

      $digest: function() {
var watch, value, last,
watchers,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
logIdx, logMsg, asyncTask; beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
$browser.$$checkUrlChange(); if (this === $rootScope && applyAsyncId !== null) {
// If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
} lastDirtyWatch = null; do { // "while dirty" loop
dirty = false;
current = target;
... traverseScopesLoop:
do { //遍历作用域
if ((watchers = current.$$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// 大部分监听都是原始的,我们只需要使用===比较即可,只有部分需要使用.equals
if (watch) {
if ((value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value === 'number' && typeof last === 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;//更新新值
watch.fn(value, ((last === initWatchVal) ? value : last), current);//执行监听函数
...
}
} catch (e) {
$exceptionHandler(e);
}
}
} // 疯狂警告: 作用域深度优先遍历
// 使得,这段代码有点疯狂,但是它有用并且我们的测试证明其有用
// 在$broadcast遍历时这个代码片段应当保持同步
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next)); // `break traverseScopesLoop;` takes us to here
if ((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: {1}',
TTL, watchLog);
} } while (dirty || asyncQueue.length); clearPhase(); while (postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
},

  至于数据的双向绑定。我们在绑定监听事件的处理函数中就已经有对$scope.name指的修改(有兴趣的可以去跟踪一下)这是其中一个方向的绑定。监听器的最前面两个监听器就保证了数据的反向绑定。第二个监听器保证了作用域的值和DOM的ng-modle中的值一致。第一个监听器则保证作用域的值和DOM的表达式的值一致。

  OK,angular的脏值检测和数据双向绑定分析就到这里。不足之处请见谅,不对的地方请各位大牛指出。

   如果觉得本文不错,请点击右下方【推荐】!

我的angularjs源码学习之旅3——脏检测与数据双向绑定的更多相关文章

  1. 我的angularjs源码学习之旅2——依赖注入

    依赖注入起源于实现控制反转的典型框架Spring框架,用来削减计算机程序的耦合问题.简单来说,在定义方法的时候,方法所依赖的对象就被隐性的注入到该方法中,在方法中可以直接使用,而不需要在执行该函数的时 ...

  2. 我的angularjs源码学习之旅1——初识angularjs

    angular诞生有好几年光景了,有Google公司的支持版本更新还是比较快,从一开始就是一个热门技术,但是本人近期才开始接触到.只能感慨自己学习起点有点晚了.只能是加倍努力赶上技术前线. 因为有分析 ...

  3. Tomcat源码学习

    Tomcat源码学习(一) 转自:http://carllgc.blog.ccidnet.com/blog-htm-do-showone-uid-4092-type-blog-itemid-26309 ...

  4. MVVM大比拼之AngularJS源码精析

    MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...

  5. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  6. jQuery源码学习感想

    还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...

  7. MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)

    前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...

  8. MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)

    前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...

  9. MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)

    前言:上篇介绍了下 MVC5 的核心原理,整篇文章比较偏理论,所以相对比较枯燥.今天就来根据上篇的理论一步一步进行实践,通过自己写的一个简易MVC框架逐步理解,相信通过这一篇的实践,你会对MVC有一个 ...

随机推荐

  1. 方维 o2o app源码出售

    方维 o2o app源码出售 方维o2oapp源码出售 1.本人官方5万购买,现把方维o2o app 源码低价出售: 2.包括网站源码本地搭建包成功提供指导 3.包括网站说明文档,不包含app说明文档 ...

  2. 使用Junit等工具进行单元测试

    一.类的定义: 类是同一事物的总称,类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称为类. 二.Junit工具的使用: 1.首先新建一个项目叫JUnit_Test,我们编写一个 ...

  3. .NET不可变集合已经正式发布

    微软基础类库(Base Class Library)团队已经完成了.NET不可变集合的正式版本,但不包括ImmutableArray.与其一起发布的还包括针对其它不可变对象类型的设计指南. 如果你需要 ...

  4. Redis高可用分布式内部交流(九)

    这是上月在公司内部的一次分享,现把PPT及交流内容整理成博客. 阅读目录: 高可用 数据同步 分布式 分布式集群时代 总结 高可用 高可用(High Availability),是当一台服务器停止服务 ...

  5. 在Github上搭建自己的博客(Windows平台)

    折腾了好久,终于在Github上搭建了自己的博客.这里面总结一下过程希望对大家能有所帮助. Github建博优缺点 和 csdn,新浪,网易相比,在Github上可以自己实现功能 和阿里云,VPS相比 ...

  6. C语言 · 矩阵乘法

    问题描述 输入两个矩阵,分别是m*s,s*n大小.输出两个矩阵相乘的结果. 输入格式 第一行,空格隔开的三个正整数m,s,n(均不超过200). 接下来m行,每行s个空格隔开的整数,表示矩阵A(i,j ...

  7. Spark使用实例

    1.介绍 Spark是基于Hadoop的大数据处理框架,相比较MapReduce,Spark对数据的处理是在本地内存中进行,中间数据不需要落地,因此速度有很大的提升.而MapReduce在map阶段和 ...

  8. iOS---数据本地化

    本篇随笔除了介绍 iOS 数据持久化知识之外,还贯穿了以下内容: (1)自定义 TableView,结合 block 从 ViewController 中分离出 View,轻 ViewControll ...

  9. Azure Service Febric 笔记:Web API应用

    1.什么是Service Febric 贴一段微软官方的介绍 Service Fabric 是一种分布式系统平台,可让你轻松打包.部署和管理可缩放.可靠的微服务.Service Fabric 还解决了 ...

  10. 使用Ado.net执行SP很慢,而用SSMS执行很快

    今天遇到一个问题,有用户反应,在site上打开报表,一直loading,出不来结果. 遇到这种问题,我立刻simulate用户使用Filter Condition,问题repro,看来不是偶然事件,通 ...