实现原理
通常angularjs 环境中变量都绑定在Scope对象上,检测这些变量变化的机制也绑定在该对象上
为了检测数据发生变化,所以为每一个变量创建监听器
监听器包含两部分内容
一个监控函数,负责前后数据对比
一个监听函数,规定对数据变化做出什么响应
但是监听器需要有机制进行触发才能发挥作用
即调用监控函数,看数据是否发生变化,然后根据结果再调用监听函数,对变化做出响应
因此,在Scope对象上挂载函数和属性实现以上机制
Scope.$$watchers = [] 用于保存注册过的所有监听器
Scope.prototype.$watch = function(watchFn, listenerFn) {} 用于将监控函数和监听函数组合成监听器,然后保存到$$watchers1
2
3
4
5
6
7Scope.prototype.$watch = function(watchFn, listenerFn) {
var watcher = {
watchFn: watchFn,//监控函数
listenerFn: listenerFn || function() { } //监听函数
};
this.$$watchers.push(watcher);
};
Scope.prototype.$$digestOnce = function() {}
具体的脏检查过程,初始化变更标记,遍历一遍$$watchers,
如果某个数据发生变化就执行对应监听函数,
并将变更标记设置true返回1
2
3
4
5
6
7
8
9
10
11
12
13
14Scope.prototype.$$digestOnce = function() {
var self = this;
var dirty;
_.forEach(this.$$watchers, function(watch) {
var newValue = watch.watchFn(self);
var oldValue = watch.last;
if (newValue !== oldValue) {
watch.listenerFn(newValue, oldValue, self);
dirty = true;
}
watch.last = newValue;
});
return dirty;
};
Scope.prototype.$digest = function() {} 循环执行检测,保证监控器内部对属性进行的变更也能被检测到1
2
3
4
5
6
7
8
9
10Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
do {
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw "10 digest iterations reached";
}
} while (dirty);
};
如果仅执行一次digest,检测不到B监听器监听函数修改的A监听器监听的属性新值,
因此进行持续遍历所有监听器,直到监控的值停止变更
$digest为$$digestOnce包裹一个do-while“外层循环”,
如果第一次运行完,有监控值发生变更了,标记为dirty,所有监听器再运行第二次。这会一直运行,直到所有监控的值都不再变化,整个局面稳定下来了。
如果两个监听器互相监控了对方产生的变更,状态始终不会稳定,因此添加迭代数量ttl,限制迭代次数,保证循环检测的可控性,达到ttl就抛异常
对于新旧值的判断 - $$areEqual
$$digestOnce中使用!==判断新旧值,可以判断值的引用但无法判断值的变更
因此在监控器里增加字段valueEq,用来定制判断方法,1
2
3
4
5
6
7
8Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn,
valueEq: !!valueEq
};
this.$$watchers.push(watcher);
};
定制判断方法,抽离成函数1
2
3
4
5
6
7
8
9Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return _.isEqual(newValue, oldValue);
} else {
return newValue === oldValue ||
(typeof newValue === 'number' && typeof oldValue === 'number' &&
isNaN(newValue) && isNaN(oldValue));//对NaN特殊情况进行处理
}
};
判断方法发生改变,进而存储方法也要根据判断方式进行改变1
2
3
4
5
6
7
8
9
10
11
12
13
14Scope.prototype.$$digestOnce = function() {
var self = this;
var dirty;
_.forEach(this.$$watchers, function(watch) {
var newValue = watch.watchFn(self);
var oldValue = watch.last;
if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) {
watch.listenerFn(newValue, oldValue, self);
dirty = true;
}
watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); //定制存储方式
});
return dirty;
};
Angular默认不使用基于值的脏检测的原因,用户需要显式设置这个标记去打开它。
集成外部代码与digest循环-$apply
1 | //$eval使用一个函数作参数,所做的事情是立即执行这个传入的函数,并且把作用域自身当作参数传递给它, |
延迟执行 - $evalAsync
1 | Scope.$$asyncQueue = [];//存储$evalAsync列入计划的任务 |
在监听函数中执行$evalAsync,digest第一次时延迟任务不会被执行,当dirty时,才会被执行
例1
2
3
4
5
6
7
8
9
10
11
12var scope = new Scope();
scope.asyncEvaled = false;
scope.$evalAsync(function(scope) {
scope.asyncEvaled = false;
});
console.log(4)
scope.$evalAsync(function(scope) {
scope.asyncEvaled = true;
});
console.log("Evaled after digest: "+scope.asyncEvaled);//true
在digest之后执行代码 - $$postDigest
1 | this.$$postDigestQueue = [];//$$postDigest函数列入计划 |
销毁一个监听器
在注册监听器时,返回一个销毁该监听器的函数,如果将来要销毁该监听器,就将返回的函数保存
在销毁时,直接执行该函数即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var self = this;
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn,
valueEq: !!valueEq
};
self.$$watchers.push(watcher);
return function() {
var index = self.$$watchers.indexOf(watcher);
if (index >= 0) {
self.$$watchers.splice(index, 1);
}
};
};
var scope = new Scope();
var removeWatch = scope.$watch(
function(scope) {
return scope.aValue;
},
function(newValue, oldValue, scope) {
scope.counter++;
}
);
removeWatch();