微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Angular 学习笔记:$digest 实现原理

\$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 = wather.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: listenerFn || function() {},last: initWatchVal
    };

    this.$$watchers.push(watcher);
}

Scope.prototype.$digest = function() {
    let self = this;
    let newValue,oldValue === initWatchVal ? newValue : oldValue,self);
        }
    });
}

循环进行脏检查

在进行 digest 时往往会发生如下情况,即某个 watcher 执行 listener 方法会引起其他 watcher 监听的对象数据发生改变,因此我们需要循环进行脏检查来使变化“彻底”完成。

Scope.prototype.$$digestOnce = function() {
    let self = this;
    let newValue,dirty;
    this.watchers.forEach((watcher) => {
        newValue = watcher.watchFn(self);
        oldValue = wather.last;
        if (newValue !== oldValue) {
            dirty = true;
            watch.last = newValue;
            watcher.listenerFn(newValue,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,dirty;
    this.watchers.forEach((watcher) => {
        newValue = watcher.watchFn(self);
        oldValue = wather.last;
        if (newValue !== oldValue) {
            self.$$lastDirtyWatch = watcher;
            dirty = true;
            watch.last = newValue;
            watcher.listenerFn(newValue,self);
        } else if (self.$$lastDirtyWatch === watcher) {
            return false;
        }
    });

    return dirty;
}

同时为了避免 \$watch 嵌套使用带来的不良影响,我们需要在每次添加 watcher 时重置 \$$lastDirtyWatch:

Scope.prototye.$watch = function(watchFn,last: initWatchVal
    };

    this.$$watchers.push(watcher);
    this.$$lastDirtyWatch = null;
}

深浅脏检查

目前为止我们实现的脏检查,仅能监听到值的变化(浅脏检查),无法判断引用内部数据发生的变化(深脏检查)。

Scope.prototye.$watch = function(watchFn,listenerFn,valueEq) {
    let watcher = {
        watchFn: watchFn,valueEq: !!valueEq,last: initWatchVal
    };

    this.$$watchers.push(watcher);
    this.$$lastDirtyWatch = null;
}
Scope.prototype.$$areEqual = function(newValue,valueEq) {
    if (valueEq) {
        return _.isEqual(newValue,oldValue);
    } else {
        return newValue === oldValue;
    }
}
Scope.prototype.$$digestOnce = function() {
    let self = this;
    let newValue,dirty;
    this.watchers.forEach((watcher) => {
        newValue = watcher.watchFn(self);
        oldValue = wather.last;
        if (!self.$$areEqual(newValue,watcher.valueEq)) {
            self.$$lastDirtyWatch = watcher;
            dirty = true;
            watch.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
            watcher.listenerFn(newValue,self);
        } else if (self.$$lastDirtyWatch === watcher) {
            return false;
        }
    });

    return dirty;
}

NaN 的兼容考虑

需要注意的是,NaN 不等于其自身,所以在判断 newValue 与 oldValue 是否相等时,需要特别考虑。

Scope.prototype.$$areEqual = function(newValue,oldValue);
    } else {
        return newValue === oldValue ||
            (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue));
    }
}

原文地址:https://www.jb51.cc/angularjs/146662.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐