My Little World

AngularJS Digest

实现原理

通常angularjs 环境中变量都绑定在Scope对象上,检测这些变量变化的机制也绑定在该对象上

为了检测数据发生变化,所以为每一个变量创建监听器
监听器包含两部分内容
一个监控函数,负责前后数据对比
一个监听函数,规定对数据变化做出什么响应

但是监听器需要有机制进行触发才能发挥作用
即调用监控函数,看数据是否发生变化,然后根据结果再调用监听函数,对变化做出响应

因此,在Scope对象上挂载函数和属性实现以上机制

Scope.$$watchers = [] 用于保存注册过的所有监听器

Scope.prototype.$watch = function(watchFn, listenerFn) {} 用于将监控函数和监听函数组合成监听器,然后保存到$$watchers

1
2
3
4
5
6
7
Scope.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
14
Scope.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
10
Scope.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
8
Scope.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
9
Scope.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
14
Scope.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
2
3
4
5
6
7
8
9
10
11
12
13
14
//$eval使用一个函数作参数,所做的事情是立即执行这个传入的函数,并且把作用域自身当作参数传递给它,
//返回的是这个函数的返回值。$eval也可以有第二个参数,它所做的仅仅是把这个参数传递给这个函数。
Scope.prototype.$eval = function(expr, locals) {
return expr(this, locals);
};
//$apply可以执行一些与Angular无关的代码(expr),这些代码也还是可以改变作用域上的东西,
//$apply可以保证作用域上的监听器可以检测这些变更。
Scope.prototype.$apply = function(expr) {
try {
return this.$eval(expr);
} finally {
this.$digest();
}
};

延迟执行 - $evalAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Scope.$$asyncQueue = [];//存储$evalAsync列入计划的任务
Scope.$$phase = null;//作用域上一个简单的字符串属性,存储了现在正在做的信息
//设置$$phase
Scope.prototype.$beginPhase = function(phase) {
if (this.$$phase) {
throw this.$$phase + ' already in progress.';
}
this.$$phase = phase;
};
//删除$$phase
Scope.prototype.$clearPhase = function() {
this.$$phase = null;
};

Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
this.$beginPhase("$digest"); //设置状态
do {
while (this.$$asyncQueue.length) {
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
this.$clearPhase();
throw "10 digest iterations reached";
}
} while (dirty);
this.$clearPhase();//移除状态
};

Scope.prototype.$apply = function(expr) {
try {
this.$beginPhase("$apply");//设置状态
return this.$eval(expr);
} finally {
this.$clearPhase();//移除状态
this.$digest();
}
};

Scope.prototype.$evalAsync = function(expr) { 添加任务
Scope.prototype.$evalAsync = function(expr) {
var self = this;
if (!self.$$phase && !self.$$asyncQueue.length) {
console.log(1)
setTimeout(function() {利用异步,连续添加任务后一定会立即digest一次
console.log(3)
if (self.$$asyncQueue.length) {
self.$digest();
}
}, 0);
}
console.log(2)
self.$$asyncQueue.push({scope: self, expression: expr});
};

};
Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
do {
while (this.$$asyncQueue.length) {
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw "10 digest iterations reached";
}
} while (dirty);
};

在监听函数中执行$evalAsync,digest第一次时延迟任务不会被执行,当dirty时,才会被执行

1
2
3
4
5
6
7
8
9
10
11
12
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
this.$$postDigestQueue = [];//$$postDigest函数列入计划
Scope.prototype.$$postDigest = function(fn) {
this.$$postDigestQueue.push(fn);
};

Scope.prototype.$digest = function() {
var ttl = 10;
var dirty;
this.$beginPhase("$digest");
do {
while (this.$$asyncQueue.length) {
try {//执行函数时,使用try-catch进行异常处理
var asyncTask = this.$$asyncQueue.shift();
this.$eval(asyncTask.expression);
} catch (e) {
(console.error || console.log)(e);
}
}
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
this.$clearPhase();
throw "10 digest iterations reached";
}
} while (dirty);
this.$clearPhase();

while (this.$$postDigestQueue.length) { //在digest最后执行
try {
this.$$postDigestQueue.shift()();
} catch (e) {
(console.error || console.log)(e);
}
}
};

销毁一个监听器

在注册监听器时,返回一个销毁该监听器的函数,如果将来要销毁该监听器,就将返回的函数保存
在销毁时,直接执行该函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Scope.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();

文档链接
完整代码