继承的作用

继承允许一个 angularjs 作用域,访问它的父作用域,一直到根作用域为止,angularjs 的继承和 js 的原型链实现大概一致,实际使用中由于继承的作用域可以访问所有的父作用域,最好还是尽可能少使用继承而使用隔离作用域

根作用域

我们一直在单独的一个 scope 上工作,这个 scope 没有父亲,它就是很典型的的根作用域,在实际使用中
我们不会使用 new 的方式来创建一个作用域,应该只有一个根作用域通过 $rootScope 注入,它的后代通过 controllers 和 directive 创建。

$new

首先要做的就是

  • 让子作用域可以访问父代作用域
  • 让父作用域不能访问子作用域
  • 子作用域可以监听父作用域的属性
  • 多个作用域不交叉

    1. it('can be nested at any depth', function() {
    2. const a = new Scope();
    3. const aa = a.$new();
    4. const aaa = aa.$new();
    5. const aab = aa.$new();
    6. const ab = a.$new();
    7. const abb = ab.$new();
    8. a.value = 1;
    9. expect(aa.value).to.equals(1);
    10. expect(aaa.value).to.equals(1);
    11. expect(aab.value).to.equals(1);
    12. expect(ab.value).to.equals(1);
    13. expect(abb.value).to.equals(1);
    14. ab.anotherValue = 2;
    15. expect(abb.anotherValue).to.equals(2);
    16. expect(aa.anotherValue).to.equals(undefined);
    17. expect(aaa.anotherValue).to.equals(undefined);
    18. });

    单例模式

    1. Scope.prototype.$new = function () {
    2. const ChildScope = function() { };
    3. ChildScope.prototype = this;
    4. const child = new ChildScope();
    5. return child;
    6. }

    要做到上面几点只需要这几条代码即可,每一个 parent 调用的 $new 都会返回拥有不同父类状态的实例,做到了作用域不交叉

    1. Scope.prototype.$new = function () {
    2. class ChildScope extends Scope{}
    3. return new ChildScope()
    4. }

    最初我的想法是上面代码,但是这样写之后,使得使用的子作用域都一致了,并没有起到作用

    影子属性

    1. it('shadows a parents property with the same name', function() {
    2. var parent = new Scope();
    3. var child = parent.$new();
    4. parent.name = 'Joe';
    5. child.name = 'Jill';
    6. expect(child.name).toBe('Jill');
    7. expect(parent.name).toBe('Joe');
    8. });

    我们不用修改代码即可做到,因为这是根据 js 的原型链的原理实现的,在子作用域上定义一个父作用域已有的属性并不会修改父作用域的值,在作用域上定义的属性,不会对父作用域有任何影响,只会对子作用域有影响。

    分散 watcher

    既然子作用域可以继承所有的方法包括 $watch 和 $digest 那么实际上,watchers 都存放在根作用域下,导致每次调用 $digest 都会遍历所有的 watchers 我们真正想要的是,只遍历所在的被调用 $digest 所在的 scope 。

    1. it('does not digest its parent(s)', function () {
    2. const parent = new Scope();
    3. const child = parent.$new();
    4. parent.aValue = 0;
    5. parent.$watch(
    6. function (scope) {return scope.aValue},
    7. function (newValue, oldValue, scope) {
    8. parent.anotherValue = 1;
    9. }
    10. )
    11. child.$digest();
    12. expect(parent.anotherValue).to.equals(undefined);
    13. });
    1. Scope.prototype.$new = function () {
    2. const ChildScope = function() {};
    3. ChildScope.prototype = this;
    4. const child = new ChildScope();
    5. child.$$watchers = [];
    6. return child;
    7. }

    由于没个 scope 都有一个数组,所以不存在影子属性

    循环 digest

    现在我们考虑到了向上 digest(调用子作用域的 $digest 不会遍历父作用域的),我们现在考虑向下 digest,考虑到子作用域可能在 watch 父作用域上的属性,但是只能遍历自己的数组,当父作用域属性改变时我们调用 $digest 时不仅会遍历自身的还会调用子作用域的,我们想要 fix 这个问题。

    1. it('can digest its child', function () {
    2. const parent = new Scope();
    3. const child = parent.$new();
    4. parent.aValue = 'abc';
    5. child.$watch(
    6. function (scope) {return scope.aValue},
    7. function (newValue, oldValue, scope) {
    8. scope.anotherValue = newValue;
    9. }
    10. )
    11. parent.$digest();
    12. expect(child.anotherValue).to.equals('abc');
    13. });

    为了实现在 parent 上调用 $digest 会执行 child 上的 watcher ,我们需要让在每一个作用域上都调用 $digest ,我们构建一个帮助函数,它会在每个子作用域上执行一次参数函数,直到这个函数返回 false。

    1. Scope.prototype.$$everyScope = function(fn) {
    2. if (fn(this)) {
    3. return this.$$children.every(function(child) {
    4. return child.$$everyScope(fn);
    5. });
    6. } else {
    7. return false;
    8. }
    9. };
    1. Scope.prototype.$$digestOnce = function () {
    2. let dirty,
    3. continueLoop = true,
    4. self = this;
    5. self.$$everyScope(function (scope) {
    6. let newValue, oldValue
    7. _.forEachRight(scope.$$watchers, function (watcher) {
    8. try {
    9. if (watcher) {
    10. newValue = watcher.watchFn(scope);
    11. oldValue = watcher.last;
    12. if (!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) {
    13. self.$$lastDirtyWatch = watcher;
    14. watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);
    15. watcher.listenerFn(newValue,
    16. (oldValue === initWatchVal ? newValue : oldValue),
    17. scope);
    18. dirty = true;
    19. } else if (self.$$lastDirtyWatch === watcher) {
    20. continueLoop = false;
    21. return false;
    22. }
    23. }
    24. } catch (err) {
    25. console.log(err);
    26. }
    27. })
    28. return continueLoop;
    29. })
    30. return dirty;
    31. }
  1. 从 parent.$digest 出发,第一次 $digestOnce 时,执行 $$everyScope ,首先在对于根作用域调用 fn(this) ,此时会遍历整个顶层作用域的 $$watchers 数组,因为 $$areEqual 此时肯定为 false ,所以 fn(this) 返回 true ,接着遍历 $$children 数组对于每个子作用域调用 fn(this) ,此时 $$areEqual 此时肯定还为 false,所以 fn(this) 返回 true,由于它现在没有 child ,所以直接返回 true 结束了 $$everyScope。
  2. 此时根作用域仍为 dirty ,且 $$lastDirtyWatch 是子作用域的最后一个 watcher ,接着再调用一次 $$digestOnce ,执行 fn(this) , 再次遍历所有子作用域,但是这次更新 $$lastDirtyWatch 。
  3. 这是因为由于只存在子作用域监听和改变父作用域上的属性,所以当子作用域的 $$everyScope 返回 false 就说明子作用域上的函数没有再改变父作用域上的值。这就是为什么 $$lastDirtyWatch 始终取的是根作用域的原因。
  4. 如果有新的赋值语句,请重新调用 parent.$digest
  5. 实际上 angularjs 没有,是用一系列 $$nextSibling, $$prevSibling, $$childHead, and $$childTail 来实现的,但是本质上和数组一样,只是会让操作消耗更少的资源。

    $apply

    现在的 $digest 只作用于该节点往下的节点,调用 $apply 的时候我们希望是从根节点往下遍历的
    1. it('digests from root on $apply', function() {
    2. const parent = new Scope();
    3. const child = parent.$new();
    4. const child2 = child.$new();
    5. parent.aValue = 'abc';
    6. parent.counter = 0;
    7. parent.$watch(
    8. function(scope) { return scope.aValue; },
    9. function(newValue, oldValue, scope) {
    10. scope.counter++;
    11. }
    12. );
    13. child2.$apply(function(scope) {});
    14. expect(parent.counter).to.equals(1);
    15. });
    1. Scope.prototype.$apply = function (expr) {
    2. try {
    3. this.$beginPhase('$apply');
    4. return this.$eval(expr);
    5. } finally {
    6. this.$clearPhase();
    7. this.$root.$digest();
    8. }
    9. };
    我们在本作用域调用 eval ,但是从根部遍历。为什么我们会让它从根部开始,因为引入外部代码,我们不确定究竟改动了哪块内容,不如直接整体遍历。如果你要节省性能最好使用 $digest 。

    $evalAsync

    1. Scope.prototype.$evalAsync = function (expr) {
    2. const self = this;
    3. if (!self.$$phase && !self.$$asyncQueue.length) {
    4. setTimeout(function () {
    5. if (self.$$asyncQueue.length) {
    6. self.$root.$digest();
    7. }
    8. }, 0);
    9. }
    10. self.$$asyncQueue.push({scope: self, expression: expr});
    11. };
    虽然 push 进了本作用域的数组,但是从根部开始遍历。

    $$lastDirtyWatch

    我们在所有用到 $$lastDirtyWatch 的地方前面加一个 $root 来保证遍历算法可以进行

    隔离作用域

    现在的子作用域和父作用域之间过于亲密,我们希望一方面子作用域仍属于继承的一部分,但是不会继承父作用域的任何属性,它从作用域链中隔离了。我们将 $new 第一个参数作为是否隔离的判断条件,隔离后不可以直接获得父作用域的值,不可以监听父作用域的任何值。
    1. Scope.prototype.$new = function(isolated) {
    2. let child;
    3. if (isolated) {
    4. child = new Scope();
    5. } else {
    6. const ChildScope = function() {};
    7. ChildScope.prototype = this;
    8. child = new ChildScope();
    9. }
    10. this.$$children.push(child);
    11. child.$$watchers = [];
    12. child.$$children = [];
    13. return child;
    14. };
    但是我们知道隔离作用域并没有完全和它的父作用域隔离,而是定义了一个 map 来说明我们可以从父作用域获取的值,我们之后会讨论

    $digest, $apply, $evalAsync, 和 $applyAsync

    由于隔离作用域我们需要重新看一遍 $digest, $apply, $evalAsync, and $applyAsync 这些函数, 后三个都从最顶层开始 digest ,$digest 在每个 scope 中都引入来 watcher 数组。
    1. it('digests from root on $apply when isolated', function() {
    2. const parent = new Scope();
    3. const child = parent.$new(true);
    4. const child2 = child.$new();
    5. parent.aValue = 'abc';
    6. parent.counter = 0;
    7. parent.$watch(
    8. function(scope) { return scope.aValue; },
    9. function(newValue, oldValue, scope) {
    10. scope.counter++;
    11. }
    12. );
    13. child2.$apply(function(scope) {});
    14. expect(parent.counter).to.equals(1);
    15. });
    我们希望 $apply 仍然从顶部作用域开始 diegst
    1. it('schedules a digest from root on $evalAsync when isolated', function(done) {
    2. const parent = new Scope();
    3. const child = parent.$new(true);
    4. const child2 = child.$new();
    5. parent.aValue = 'abc';
    6. parent.counter = 0;
    7. parent.$watch(
    8. function(scope) { return scope.aValue; },
    9. function(newValue, oldValue, scope) {
    10. scope.counter++;
    11. }
    12. );
    13. child2.$evalAsync(function(scope) {});
    14. setTimeout(function() {
    15. expect(parent.counter).to.equals(1);
    16. done();
    17. }, 50);
    18. });
    $evalAsync 同理。
    1. Scope.prototype.$new = function(isolated) {
    2. let child;
    3. if (isolated) {
    4. child = new Scope();
    5. child.$root = this.$root;
    6. child.$$asyncQueue = this.$$asyncQueue;
    7. child.$$postDigestQueue = this.$$postDigestQueue;
    8. child.$$applyAsyncQueue = this.$$applyAsyncQueue;
    9. } else {
    10. const ChildScope = function() {};
    11. ChildScope.prototype = this;
    12. child = new ChildScope();
    13. }
    14. this.$$children.push(child);
    15. child.$$watchers = [];
    16. child.$$children = [];
    17. return child;
    18. };
    child.$root,child.$$asyncQueue,child.$$postDigestQueue,$$applyAsyncQueue,这四个利用父作用域的影子属性拿到顶层作用域的值。
    1. it("executes $applyAsync functions on isolated scopes", function(done) {
    2. const parent = new Scope();
    3. const child = parent.$new(true);
    4. let applied = false;
    5. parent.$applyAsync(function() {
    6. applied = true;
    7. });
    8. child.$digest();
    9. expect(applied).to.equals(true);
    10. });
    我们希望 child 调用 digest 时,会让遍历顶层的数组,但是隔离作用域进行 $$applyAsyncId 判断时会创建一个新的属性,而不是使用顶层的所以总是为 undefined ,只有在任何使用到 $$applyAsyncId 的地方前面加上 $root 即可。

    销毁 scope

    在实际程序运行过程中,时刻存在着 scope 作用域的膨胀和缩小 ```jsx Scope.prototype.$destroy = function() { if (this.$parent) {
    1. const siblings = this.$parent.$$children;
    2. const indexOfThis = siblings.indexOf(this);
    3. if (indexOfThis >= 0) {
    4. siblings.splice(indexOfThis, 1);
    5. }
    } this.$$watchers = null; };

Scope.prototype.$new = function(isolated, parent) { let child; parent = parent || this; if (isolated) { child = new Scope(); child.$root = this.$root; child.asyncQueue = this.asyncQueue; child.postDigestQueue = this.postDigestQueue; child.applyAsyncQueue = this.applyAsyncQueue; } else { const ChildScope = function() {}; ChildScope.prototype = this; child = new ChildScope(); } this.children.push(child); child.watchers = []; child.$$children = []; child.$parent = parent; return child; }; ```