前言


  为了后面描述方便,我们将保存模块的对象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. XML数据的解析

    XML数据的解析 相比于JSON数据解析而言,XML数据解析可能会让更多的童鞋感觉到吃力,对我来说,同样认为JSON数据好像让人感觉比较友好,不过对于程序开发者来说,无非就是这两种数据解析占比较大的部 ...

  2. [.net 面向对象程序设计进阶] (14) 缓存(Cache) (一) 认识缓存技术

    [.net 面向对象程序设计进阶] (14) 缓存(Cache)(一) 认识缓存技术 本节导读: 缓存(Cache)是一种用空间换时间的技术,在.NET程序设计中合理利用,可以极大的提高程序的运行效率 ...

  3. JavaScript实现TwoQueues缓存模型

    本文所指TwoQueues缓存模型,是说数据在内存中的缓存模型. 无论何种语言,都可能需要把一部分数据放在内存中,避免重复运算.读取.最常见的场景就是JQuery选择器,有些Dom元素的选取是非常耗时 ...

  4. 代码提交的时候可以插入表情了-GitHub表情的使用

    GitHub官方有个表情项目,旨在丰富文字信息.意味着你可以在提交代码的时候,在提交信息里面添加表情,同时也可以在项目的ReadMe.md文件里面使用表情.除此之外,当然还有项目在GitHub上的wi ...

  5. SharpFileDB - a file database for small apps

    SharpFileDB - a file database for small apps 本文中文版在此处. I'm not an expert of database. Please feel fr ...

  6. Hadoop 裡的 fsck 指令

    Hadoop 裡的 fsck 指令,可檢查 HDFS 裡的檔案 (file),是否有 corrupt (毀損) 或資料遺失,並產生 HDFS 檔案系統的整體健康報告.報告內容,包括:Total blo ...

  7. Module Zero之用户管理

    返回<Module Zero学习目录> 用户实体 用户管理者 用户认证 用户实体 用户实体代表应用的一个用户,它派生自AbpUser类,如下所示: public class User : ...

  8. 电脑桌面 IE 图标删除不了的解决方法

    电脑换了系统之后想把桌面的IE浏览器给删掉,可是直接删除又删不掉,杀毒软件查杀也没有问题.找了很多方法,终于才把它给解决了.下面,就把我的方法分享给桌面ie图标删除不了的解决方法,希望能对大家有所帮助 ...

  9. iOS-应用闪退总结

    一.之前上架的 App 在 iOS 9 会闪退问题(iOS系统版本更新,未配置新版本导致闪退问题) 最新更新:(2015.10.02) 开发环境: Delphi 10 Seattle OS X El ...

  10. XE1:使用SSMS创建Extended Events

    Extended Events 用于取代SQL trace,是SQL Server 追踪系统运行的神器,其创建过程十分简单. 一,创建Extended Events的Session step1,打开N ...