我的angularjs源码学习之旅3——脏检测与数据双向绑定
前言
为了后面描述方便,我们将保存模块的对象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的脏值检测和数据双向绑定分析就到这里。不足之处请见谅,不对的地方请各位大牛指出。