Angular源码解析2--——脏值查询完善

上一篇提到的问题

上一篇提到了由于监听函数第三个传参是作用域本身,所以我们可以在监听函数内部改变作用域的值。但是这么做的话,会有另一个监控器来监控值的变化。但是在一次$digest中就无法检测值的变化。所以我们需要实现一种机制——$digest会持续遍历所有监听器,知道他们停止变化。

实现原理

我们将$digest改名为$$digestOnce。意思是每调用他一次就会遍历所有监听器一次。他会返回一个状态,表示这次遍历该数据是否发生变化。我们再用一个外层函数包裹$$digestOnce,一旦发现这个状态为true,就再执行一次$$$digestOnce。直到状态不再为true。这表示数据不再发生变化。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Scope.prototype.$$$digestOnce=function () {
var that=this;
var dirty;
this.$$watchers.forEach(function (watch){
var oldVal=watch.last;
var newVal=watch.watchFn(that);
if (oldVal!=newVal) {
watch.listenerFn(newVal,oldVal,that);
dirty=true;
}
watch.last=newVal;
})
return dirty;
}
Scope.prototype.$digest=function () {
var dirty;
do {
dirty=this.$$digestOnce();
} while (dirty)
}

重要提示

  • 其实Angular的源码中并不存在$$digestOnce,而是将其直接封装在$digest中,这里只是为了结构的清晰。
  • 通过这里我们可以看出,在一次$digest中,监听器可能会执行多次。

漏洞

假设我们有两个监听器,监听器a监听Scope.a的值并且他的监听函数会改变Scope.b的值。监听器b监听Scope.b的值并且他的监听函数会改变Scope.a的值。这样互相监听,最终会导致双方的值永远无法稳定,$digest会无限调用,这肯定是需要被杜绝的。

应对

对此,我们需要设定,当$digest执行一定次数以后,如果状态还是无法稳定,我们就宣布他的状态永远无法稳定,并抛出错误。为了性能考虑,Angular对这个次数的设定是10。

代码实现

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)
}

基于值的检测与基于地址的检测

我们之前对比oldVal与newVal时,使用的是’===’操作符,这在检测基本值类型的数据时是完全没有问题的。当检测引用类型的值时比较的是引用的地址。但是有时候我们需要检测对象或数组内部数据的变更,这时候我们就需要基于值的检测,而不是基于地址的检测了。
由于基于值的检测时一项比较消耗性能(相对)的操作,所以我们应该提供一个可选参数,让使用者自己决定在什么时候开启(全等比较函数源码见下篇)。

代码实现

1
2
3
4
5
6
7
8
9
Scope.porototype.$watch=function (watchFn,listenerFn,valueEq) {
var watcher={
watchFn:watchFn,
listenerFn:listenerFn,
//这里通过两次取反,使得强制转换成布尔类型,如果未传递值undefined会被转换成false
valueEq:!!valueEq
};
this.$$watchers.push(watcher);
}

基于值的检测意味着如果数据时对象或数组类型,脏值查询会遍历数据的每一项,如果数据存在嵌套的数组或对象,还会递归的按值进行比较。

另一个问题

这里我们的深度检测只发生在newVal与oldVal比较的时候。回忆下,我们的新值会保存在watch.last中。试想如果保存的是一个引用类型的值,那么同一个值通过watchFn取得的和watch.last取得的其实是同一个地址。也就是说watch.fn(旧值)的变化会实时表现在watch.last(新值)中。这样就无法检测到引用类型值的变化了。
所以我们保存在watch.last中的数据应该是一个值,而不是一个地址。也就是说我们需要深度克隆一份引用类型的值。(源码分析见下一篇)

源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scope.prototype.$$digestOnce = function() {
var that = this;
var dirty;
this.$$watchers.forEach(function(watch) {
var newValue = watch.watchFn(that);
var oldValue = watch.last;
if (!that.$$areEqual(newValue, oldValue, watch.valueEq)) {
watch.listenerFn(newValue, oldValue, that);
dirty = true;
}
watch.last = (watch.valueEq ? this.$$cloneDeep(newValue) : newValue);
});
return dirty;
};

最后一点小问题

在原生js中NaN!==NaN。如果我们的数据中包含NaN,在进行引用类型检测时(值类型的检测我们封装的$$areEqual已经帮我们处理了)由于NaN!==NaN那么这个值会始终是脏的。所以我们需要手动处理他。

源码实现

1
2
3
4
5
6
7
8
9
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return this.$$isEqual(newValue, oldValue);
} else {
return newValue === oldValue ||
(typeof newValue === 'number' && typeof oldValue === 'number' &&
isNaN(newValue) && isNaN(oldValue));
}
};