双向绑定的三个重要方法:
- $scope.$apply()
- $scope.$digest()
- $scope.$watch()
一、$scope.$watch()
我理解的$watch就是将对某个数据的监听器对象存储在$scope下。当给$watch指定如下两个函数,就可以创建一个监听器:
- 一个监控函数,我们通常传进去的是一个表达式,比如说“user.firstName”,但框架本身实际上是调用了一个函数,返回指定所关注的那部分数据。
- 一个监听函数,用于在数据变更的时候接受提示。
为了实现$watch,我们需要存储监听器对象。在Scope构造函数上添加一个数组:
function Scope() {
this.$$watchers = [];
}
$$在angular中表示这个变量被当作私有的来考虑,不应当在外部代码中调用。
现在我们正式定义$watch()方法,源代码如下所示:
$watch: function(watchExp, listener, objectEquality) {
var scope = this,
get = compileToFn(watchExp, 'watch'),
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: watchExp,
eq: !!objectEquality
}; // in the case user pass string, we need to compile it, do we really need this ?
if (!isFunction(listener)) {
var listenFn = compileToFn(listener || noop, 'listener');
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
} if (typeof watchExp == 'string' && get.constant) {
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
} if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher); return function() {
arrayRemove(array, watcher);
};
},
其中$$watchers就是wo我们上述的scope中存储监听器的数组,$watch()通过unshift()方法将监听器对象加入数组。
二、$scope.$digest()
$digest函数的作用是简而言之就是作用域上遍历所有监听器,也就是$scope.$$watchers,调用每个监听器对象下的监控函数,并且比较它返回的值和上一次返回值的差异。如果不相同,监听器就是脏的,它的监听函数就应当被调用。源代码如下所示:
$digest: function() {
var watch, value, last,
watchers,
asyncQueue = this.$$asyncQueue,
postDigestQueue = this.$$postDigestQueue,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
logIdx, logMsg, asyncTask; beginPhase('$digest'); do { // "while dirty" loop
dirty = false;
current = target; while(asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch (e) {
$exceptionHandler(e);
}
} do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch && (value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value == 'number' && typeof last == 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
logMsg = (isFunction(watch.exp))
? 'fn: ' + (watch.exp.name || watch.exp.toString())
: watch.exp;
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
}
}
} catch (e) {
$exceptionHandler(e);
}
}
} // Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next)); if(dirty && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}',
TTL, toJson(watchLog));
}
} while (dirty || asyncQueue.length); clearPhase(); while(postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
},
三、$scope.$apply()
$apply使用函数作参数,它用$eval执行这个函数,然后通过$digest触发digest循环。源代码如下所示:
$apply: function(expr) {
try {
beginPhase('$apply');
return this.$eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
clearPhase();
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
},
可以看到,在apply方法里其实是调用了digest方法的,那么为什么要多增加一个apply来调用digest呢,可以看到这段代码中并没有直接调用digest而是首先进行了对expr的检验,也就是eval方法,这个方法如果校验不通过,是会抛出异常的,而angular并不推荐外部直接调用digest,所以就增加了apply方法来间接调用。
利用$apply(),我们可以执行一些与Angular无关的代码,这些代码也还是可以改变作用域上的东西,$apply可以保证作用域上的监听器可以检测这些变更。
四、何时执行和跳出digest loop呢?
引用网上一张图来解释
资料引用自:http://my.oschina.net/brant/blog/419641