$watch 和 $digest
$watch
和 $digest
是数据绑定中的核心概念:我们可以使用 $watch 在 scope 中绑定 watcher
用于监听 scope 中发生的变化,而 $digest 方法的执行即是遍历 scope 上绑定的所有 watcher,并执行相应的 watch(指定想要监控的对象) 和 listener(当数据改变时触发的回调) 方法。
function Scope {
this.$$watchers = []; // $$ 前缀表示私有变量
}
Scope.prototye.$watch = function(watchFn, listenerFn) {
let watcher = {
watchFn: watchFn,
listenerFn: listenerFn,
};
this.$$watchers.push(watcher);
}
Scope.prototype.$digest = function() {
this.watchers.forEach((watcher) => {
watcher.listenerFn();
});
}
上述代码实现的 $digest 并不实用,因为实际上我们需要的是:监听的对象数据发生改变时才执行相应的 listener 方法。
脏检查
Scope.prototype.$digest = function() {
let self = this;
let newValue, oldValue;
this.watchers.forEach((watcher) => {
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if (newValue !== oldValue) {
watch.last = newValue;
watcher.listenerFn(newValue, oldValue, self);
}
});
}
上述代码在大部分情况下可以正常运行,但是当我们首次遍历 watcher 对象时其 last
变量值为 undefined
,这样会导致如果 watcher 的第一个有效值同为 undefined 不会触发 listener 方法。
console.log(undefined === undefined) // true
我们使用 initWatchVal
方法解决这个问题.
function initWatchVal() {
// TODO
}
Scope.prototye.$watch = function(watchFn, listenerFn) {
let watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() {},
last: initWatchVal
};
this.$$watchers.push(watcher);
}
Scope.prototype.$digest = function() {
let self = this;
let newValue, oldValue;
this.watchers.forEach((watcher) => {
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if (newValue !== oldValue) {
watch.last = newValue;
watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
}
});
}
循环进行脏检查
在进行 digest 时往往会发生如下情况,即某个 watcher 执行 listener 方法会引起其他 watcher 监听的对象数据发生改变,因此我们需要循环进行脏检查来使变化“彻底”完成。
Scope.prototype.$$digestOnce = function() {
let self = this;
let newValue, oldValue, dirty;
this.watchers.forEach((watcher) => {
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if (newValue !== oldValue) {
dirty = true;
watch.last = newValue;
watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
}
});
return dirty;
}
Scope.prototype.$digest = function() {
let dirty;
do { dirty = this.$$digestOnce(); }
while (dirty);
}
上述代码只要在遍历中发现脏值,就会多循环一轮直到没有发现脏值为止,我们考虑这样的情况:即是两个 watcher 之间互相影响彼此,则会导致无限循环的问题。
我们使用 TTL
(Time to Live)来约束遍历的最大次数,在 Angular 中默认次数为10。
Scope.prototype.$digest = function() {
let dirty;
let ttl = 10;
do {
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw '10 digest iterations reached.';
}
} while (dirty)
}
同时,在每次 digest 的最后一轮遍历没有必要对全部 watcher 进行检查,我们通过使用 $$lastDirtyWatch
变量来对这部分代码的性能进行优化。
function Scope {
this.$$watchers = [];
this.$$lastDirtyWatch = null;
}
Scope.prototype.$digest = function() {
let dirty;
let ttl = 10;
this.$$lastDirtyWatch = null;
do {
dirty = this.$$digestOnce();
if (dirty && !(ttl--)) {
throw '10 digest iterations reached.';
}
} while (dirty)
}
Scope.prototype.$$digestOnce = function() {
let self = this;
let newValue, oldValue, dirty;
this.watchers.forEach((watcher) => {
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if (newValue !== oldValue) {
self.$$lastDirtyWatch = watcher;
dirty = true;
watch.last = newValue;
watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
} else if (self.$$lastDirtyWatch === watcher) {
return false;
}
});
return dirty;
}
同时为了避免 $watch 嵌套使用带来的不良影响,我们需要在每次添加 watcher 时重置 $$lastDirtyWatch:
Scope.prototye.$watch = function(watchFn, listenerFn) {
let watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() {},
last: initWatchVal
};
this.$$watchers.push(watcher);
this.$$lastDirtyWatch = null;
}
深浅脏检查
目前为止我们实现的脏检查,仅能监听到值的变化(浅脏检查),无法判断引用内部数据发生的变化(深脏检查)。
Scope.prototye.$watch = function(watchFn, listenerFn, valueEq) {
let watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() {},
valueEq: !!valueEq,
last: initWatchVal
};
this.$$watchers.push(watcher);
this.$$lastDirtyWatch = null;
}
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return _.isEqual(newValue, oldValue);
} else {
return newValue === oldValue;
}
}
Scope.prototype.$$digestOnce = function() {
let self = this;
let newValue, oldValue, dirty;
this.watchers.forEach((watcher) => {
newValue = watcher.watchFn(self);
oldValue = watcher.last;
if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {
self.$$lastDirtyWatch = watcher;
dirty = true;
watch.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
watcher.listenerFn(newValue, oldValue === initWatchVal ? newValue : oldValue, self);
} else if (self.$$lastDirtyWatch === watcher) {
return false;
}
});
return dirty;
}
NaN 的兼容考虑
需要注意的是,NaN 不等于其自身,所以在判断 newValue 与 oldValue 是否相等时,需要特别考虑。
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));
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。