前言


  为了后面描述方便,我们将保存模块的对象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. perl 遍历对象数组

    my $appsList ; eval { $appsList = $db->query( $sqlstr1 )->hashes->to_array; }; ### $appsLis ...

  2. 有了lisk,为什么我们还要做一个Asch?

    0 前言 首先要声明一点,我们和我们的一些朋友都是lisk的投资人和支持者,我们也相信lisk会成功. 事实上,lisk已经成功了一半,目前在区块链领域融资金额排行第二,仅次于以太坊. 那为什么我们还 ...

  3. [转]runtime 消息机制

    原文地址:http://www.jianshu.com/p/f6300eb3ec3d 一.关于runtime 之前在项目中有遇到过用runtime解决改变全局字体的问题,所以再一次感受到了runtim ...

  4. 非域环境下搭建自动故障转移镜像无法将 ALTER DATABASE 命令发送到远程服务器实例的解决办法

    非域环境下搭建自动故障转移镜像无法将 ALTER DATABASE 命令发送到远程服务器实例的解决办法 环境:非域环境 因为是自动故障转移,需要加入见证,事务安全模式是,强安全FULL模式 做到最后一 ...

  5. 【腾讯优测干货分享】Android 相机预览方向及其适配探索

    本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/583ba1df25d735cd2797004d 由于Android系统的开放策略 ...

  6. python自动化测试(3)- 自动化框架及工具

    python自动化测试(3) 自动化框架及工具 1   概述 手续的关于测试的方法论,都是建立在之前的文章里面提到的观点: 功能测试不建议做自动化 接口测试性价比最高 接口测试可以做自动化 后面所谈到 ...

  7. 剑指Offer面试题:18.二叉树的镜像

    一.题目:二叉树的镜像 题目:请完成一个函数,输入一个二叉树,该函数输出它的镜像.例如下图所示,左图是原二叉树,而右图则是该二叉树的镜像. 该二叉树节点的定义如下,采用C#语言描述: public c ...

  8. 【效率】专为Win7系统设计的极简番茄计时器 - MiniPomodoro (附源码)

    时光飞逝,一转眼坚持使用番茄工作法已经快3年了!能坚持这么长时间,主要还是得益于它的简单.但是令人纠结的是,这么长时间以来,换了7款不同的番茄计时器,仍然没有找到非常满意的: ■ 机械的噪音太大,会妨 ...

  9. 我只是想开个饭店—— JavaIO模型的演变

    Java的IO...真的是我所见过的高级语言中.最最复杂的... 看着这个图我也是醉了. 但是不知不觉间,java的IO已经更新到了NIO.2了,IO库早已经不止是这个样子了,那么这个过程中,它们经历 ...

  10. 《App研发录》 源码

    第1章源码: 1.1 重新规划Android项目结构 1.1.zip 1.2 为Activity定义新的生命周期 1.2.zip 1.3 统一事件编程模型 1.3.zip 1.4 实体化编程 1.4. ...