AngularJS源码解析3:RootScope的创建过程

RootScopeProvider简介

RootScopeProvider是angularjs里面比较活跃的一个provider。它主要用来生成实例rootScope,它代表angularjs应用的根作用域。我们可以把它看成MVVM模式中的VM。

源代码如下:

function $RootScopeProvider(){
        var TTL = 10;
        var $rootScopeMinErr = minErr(‘$rootScope‘);
        var lastDirtyWatch = null;

        this.digestTtl = function(value) {
            if (arguments.length) {
                TTL = value;
            }
            return TTL;
        };
        this.$get = [‘$injector‘, ‘$exceptionHandler‘, ‘$parse‘, ‘$browser‘,
            function( $injector,   $exceptionHandler,   $parse,   $browser) {
                .......
            }];
    }

在$RootScopeProvider构造函数中,有一个$get属性,它的值是一个数组,数组的最后的一项是一个函数function( $injector,   $exceptionHandler,   $parse,   $browser) ,所有的代码都在这个方法中。angularJS内置的provider都有一个$get属性,此属性值主要是定义实例化provider对象的函数体,实例化时,会通过instanceinjector.invoke来调用。在$get属性上面还有一个digestTtl属性,这个属性是用来修改angularJS默认的dirty check次数,angularJS默认是10次。接下来,我们来看下$get属性值中的实例化

$RootScopeProvider的函数体。

          function Scope() {
                    this.$id = nextUid();
                    this.$$phase = this.$parent = this.$$watchers =
                        this.$$nextSibling = this.$$prevSibling =
                            this.$$childHead = this.$$childTail = null;
                    this[‘this‘] = this.$root =  this;
                    this.$$destroyed = false;
                    this.$$asyncQueue = [];
                    this.$$postDigestQueue = [];
                    this.$$listeners = {};
                    this.$$listenerCount = {};
                    this.$$isolateBindings = {};
                }
                Scope.prototype = {
                    constructor: Scope,
                    $new: function(isolate) {
                        .....
                    },
                    $watch: function(watchExp, listener, objectEquality) {
                       ......
                    },
                    $watchCollection: function(obj, listener) {
                        var self = this;
                        var oldValue;
                        var newValue;
                        var changeDetected = 0;
                        var objGetter = $parse(obj);
                        var internalArray = [];
                        var internalObject = {};
                        var oldLength = 0;
                        function $watchCollectionWatch() {
                            newValue = objGetter(self);
                            var newLength, key;
                            if (!isObject(newValue)) {
                                if (oldValue !== newValue) {
                                    oldValue = newValue;
                                    changeDetected++;
                                }
                            } else if (isArrayLike(newValue)) {
                                if (oldValue !== internalArray) {
                                    oldValue = internalArray;
                                    oldLength = oldValue.length = 0;
                                    changeDetected++;
                                }
                                newLength = newValue.length;
                                if (oldLength !== newLength) {
                                    changeDetected++;
                                    oldValue.length = oldLength = newLength;
                                }
                                for (var i = 0; i < newLength; i++) {
                                    if (oldValue[i] !== newValue[i]) {
                                        changeDetected++;
                                        oldValue[i] = newValue[i];
                                    }
                                }
                            } else {
                                if (oldValue !== internalObject) {
                                    oldValue = internalObject = {};
                                    oldLength = 0;
                                    changeDetected++;
                                }
                                newLength = 0;
                                for (key in newValue) {
                                    if (newValue.hasOwnProperty(key)) {
                                        newLength++;
                                        if (oldValue.hasOwnProperty(key)) {
                                            if (oldValue[key] !== newValue[key]) {
                                                changeDetected++;
                                                oldValue[key] = newValue[key];
                                            }
                                        } else {
                                            oldLength++;
                                            oldValue[key] = newValue[key];
                                            changeDetected++;
                                        }
                                    }
                                }
                                if (oldLength > newLength) {
                                    changeDetected++;
                                    for(key in oldValue) {
                                        if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
                                            oldLength--;
                                            delete oldValue[key];
                                        }
                                    }
                                }
                            }
                            return changeDetected;
                        }
                        function $watchCollectionAction() {
                            listener(newValue, oldValue, self);
                        }
                        return this.$watch($watchCollectionWatch, $watchCollectionAction);
                    },
                    $digest: function() {
                        ........
                    },
                    $destroy: function() {
                        ......
                    },
                    $eval: function(expr, locals) {
                        .....
                    },
                    $evalAsync: function(expr) {
                        .......
                    },
                    $$postDigest : function(fn) {
                        ........
                    },
                    $apply: function(expr) {
                        ......
                    },
                    $on: function(name, listener) {
                        ......
                    },
                    $emit: function(name, args) {
                        .......
                    },
                    $broadcast: function(name, args) {
                      ......
                    }
                };
                var $rootScope = new Scope();
                return $rootScope;
                function beginPhase(phase) {
                    if ($rootScope.$$phase) {
                        throw $rootScopeMinErr(‘inprog‘, ‘{0} already in progress‘, $rootScope.$$phase);
                    }
                    $rootScope.$$phase = phase;
                }
                function clearPhase() {
                    $rootScope.$$phase = null;
                }
                function compileToFn(exp, name) {
                    var fn = $parse(exp);
                    assertArgFn(fn, name);
                    return fn;
                }
                function decrementListenerCount(current, count, name) {
                    do {
                        current.$$listenerCount[name] -= count;
                        if (current.$$listenerCount[name] === 0) {
                            delete current.$$listenerCount[name];
                        }
                    } while ((current = current.$parent));
                }
                function initWatchVal() {}

从代码中,可以看到。第一步,定义了一个构造函数function Scope(){},第二步,定义了Scope的原型Scope.prototype = {},第三步,new出来了一个Scope,赋值给了$rootScope,并return这个Scope实例对象。

通过第三步大家可以看出,angularJS默认会创建根作用域$rootScope,并作为$rootScopeprovider的实例对象返回。

第一步,初始化了Scope对象的一些属性值。

第二步,定义了一些方法。我们来详细讲解这些方法的作用:

$new: function(isolate) {
                        var ChildScope,
                            child;

                        if (isolate) {
                            child = new Scope();
                            child.$root = this.$root;
                            // ensure that there is just one async queue per $rootScope and its children
                            child.$$asyncQueue = this.$$asyncQueue;
                            child.$$postDigestQueue = this.$$postDigestQueue;
                        } else {
                            ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges
                            // the name it does not become random set of chars. This will then show up as class
                            // name in the web inspector.
                            ChildScope.prototype = this;
                            child = new ChildScope();
                            child.$id = nextUid();
                        }
                        child[‘this‘] = child;
                        child.$$listeners = {};
                        child.$$listenerCount = {};
                        child.$parent = this;
                        child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
                        child.$$prevSibling = this.$$childTail;
                        if (this.$$childHead) {
                            this.$$childTail.$$nextSibling = child;
                            this.$$childTail = child;
                        } else {
                            this.$$childHead = this.$$childTail = child;
                        }
                        return child;
 },

此方法的作用是用来创建子作用域。

  • isolate变量的值决定是执行if语句的代码还是else语句的代码,如果isolate的值是true,那么就会创建一个独立的作用域。这个在我们创建指令,并且在创建指令的回调方法中有scope属性的情况下,会出现这种情况,当然还有其他别的特殊情况下也会这样。假如是独立作用域的话,会多一个$root属性,它的值默认指向rootscope。

  • 如果isolate的值为false,则会定义一个空的构造函数ChildScope ,并且把此构造函数的prototype指向当前scope的实例对象,然后,new一个ChildScope 对象。

  • 然后为生成的子作用域设置$parent属性为当前作用域,并且设置子作用域的一些默认的属性值,然后设置当前作用域的一些属性值为生成的子作用域。最后,返回这个新生成的子作用域child。

接下来,我们再来说说$watch方法:

$watch: function(watchExp, listener, objectEquality) {
                        var scope = this,
                            get = compileToFn(watchExp, ‘watch‘),
                            array = scope.$$watchers,
                            watcher = {
                                fn: listener,
                                last: initWatchVal,
                                get: get,
                                exp: watchExp,
                                eq: !!objectEquality
                            };
                        lastDirtyWatch = null;
                        if (!isFunction(listener)) {
                            var listenFn = compileToFn(listener || noop, ‘listener‘);
                            watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
                        }
                        if (typeof watchExp == ‘string‘ && get.constant) {
                            var originalFn = watcher.fn;
                            watcher.fn = function(newVal, oldVal, scope) {
                                originalFn.call(this, newVal, oldVal, scope);
                                arrayRemove(array, watcher);
                            };
                        }
                        if (!array) {
                            array = scope.$$watchers = [];
                        }
                        array.unshift(watcher);
                        return function() {
                            arrayRemove(array, watcher);
                            lastDirtyWatch = null;
                        };
}

$watch方法有三个参数,第一个是监听表达式,可以是字符串也可以是方法,第二个是监听方法,第三个参数代表是否深度监听。

方法里面首先初始化一个get局部变量,初始化get的compileToFn函数其实是调用$parse实例对象来解析监听参数的,它会返回一个方法赋给get属性,这个方法会在dirty check里用到,用来获取监听表达式的值。$parse实例对象是$parseprovider实例化生成的。

接下来,初始化了一个watcher对象,此对象用来保存一些监听相关的信息:

  • fn: 代表监听方法,当监听表达式有变化时,也就说值改变时会执行此方法
  • last: 保存最后一次发生变化的监听表达式的值
  • get: 保存一个监听表达式对应的方法,目的是用来获取表达式的值然后用来进行新旧对比,看是否有变化
  • exp: 保存一个原始的监听表达式
  • eq: 保存$watch方法的第三个参数,表示是否进行深度比较

然后会检查传递进来的监听方法是否为方法,如果不是,则通过parse方法解析生成一个方法listenFn ,然后通过包装这个listenFn方法生成一个方法watcher.fn

,此方法就是被称为监听方法,并且此方法体的内容就是执行刚才生成的方法listenFn,并默认传递当前作用域作为参数。

接着会检查监听表达式是否为字符串,如果是并且监听表达式的constant为true,就进入if语句,这代表监听表达式这个字符串是一个常量。那么,angular在处理这种监听的时候,执行完一次监听方法之后就会删除这个$watch。

最后,往当前作用域里的$$watchers数组中添加$watch对象,并返回一个方法。注意这里的返回值,利用JS的闭包保留了当前的watcher变量,这个返回值方法是用来删除监听用的。

接下来,我们来说说$digest方法

digest方法是dirty check的核心,它里面的代码是先执行$$asyncQueue队列中保存的表达式,然后开启一个traverseScopesLoop循环,来循环遍历$$watchers,如果watch与上一次的值不相同,也就是被改变了,就执行watch里的监听方法。假如ttl超过了angular默认设置的值,则dirth check结束。最后执行$$postDigestQueue队列里的表达式。

$digest: function() {
                        var watch, value, last,
                            watchers,
                            asyncQueue = this.$$asyncQueue,
                            postDigestQueue = this.$$postDigestQueue,
                            length,
                            dirty, ttl = TTL,
                            next, current, target = this,
                            watchLog = [],
                            logIdx, logMsg, asyncTask;
                        beginPhase(‘$digest‘);
                        lastDirtyWatch = null;
                        do { 
                            dirty = false;
                            current = target;
                            while(asyncQueue.length) {
                                try {
                                    asyncTask = asyncQueue.shift();
                                    asyncTask.scope.$eval(asyncTask.expression);
                                } catch (e) {
                                    clearPhase();
                                    $exceptionHandler(e);
                                }
                                lastDirtyWatch = null;
                            }
                            traverseScopesLoop:
                                do { 
                                    if ((watchers = current.$$watchers)) {
                                        length = watchers.length;
                                        while (length--) {
                                            try {
                                                watch = watchers[length];
                                                if (watch) {
                                                    if ((value = watch.get(current)) !== (last = watch.last) &&
                                                        !(watch.eq
                                                            ? equals(value, last)
                                                            : (typeof value == ‘number‘ && typeof last == ‘number‘
                                                            && isNaN(value) && isNaN(last)))) {
                                                        dirty = true;
                                                        lastDirtyWatch = watch;
                                                        watch.last = watch.eq ? copy(value) : value;
                                                        watch.fn(value, ((last === initWatchVal) ? value : last), current);
                                                        if (ttl < 5) {
                                                            logIdx = 4 - ttl;
                                                            if (!watchLog[logIdx]) watchLog[logIdx] = [];
                                                            logMsg = (isFunction(watch.exp))
                                                                ? ‘fn: ‘ + (watch.exp.name || watch.exp.toString())
                                                                : watch.exp;
                                                            logMsg += ‘; newVal: ‘ + toJson(value) + ‘; oldVal: ‘ + toJson(last);
                                                            watchLog[logIdx].push(logMsg);
                                                        }
                                                    } else if (watch === lastDirtyWatch) {
                                                        dirty = false;
                                                        break traverseScopesLoop;
                                                    }
                                                }
                                            } catch (e) {
                                                clearPhase();
                                                $exceptionHandler(e);
                                            }
                                        }
                                    }
                                    if (!(next = (current.$$childHead ||
                                        (current !== target && current.$$nextSibling)))) {
                                        while(current !== target && !(next = current.$$nextSibling)) {
                                            current = current.$parent;
                                        }
                                    }
                                } while ((current = next));
                            if((dirty || asyncQueue.length) && !(ttl--)) {
                                clearPhase();
                                throw $rootScopeMinErr(‘infdig‘,
                                    ‘{0} $digest() iterations reached. Aborting!\n‘ +
                                        ‘Watchers fired in the last 5 iterations: {1}‘,
                                    TTL, toJson(watchLog));
                            }
                        } while (dirty || asyncQueue.length);
                        clearPhase();
                        while(postDigestQueue.length) {
                            try {
                                postDigestQueue.shift()();
                            } catch (e) {
                                $exceptionHandler(e);
                            }
                        }
},

通过上面的代码,可以看出,里面的核心就是两个循环,外循环保证所有的model都能检测到(do{.....} while (dirty || asyncQueue.length);),内循环则是真正的检测每个watch(traverseScopesLoop: do{......}while((current = next));)。

watch.get方法的作用是计算监听表达式的值,然后用当前的值去跟旧值进行对比,假如不相等,也就是改变了,就执行监听方法。

注意这里的watch.eq,它的作用是是否进行深度检查,equals方法是angular里的方法,用来深度对比两个对象。这里的不相等有一个例外,那就是NaN === NaN,这个永远都是false,所以这里加了判断。

比较完之后,把新值传给watch.last,然后执行watch.fn,也就是监听方法,传递三个参数给它,分别是:最新计算的值,上次计算的值(如果是第一次,则传递新值),最后一个参数是当前作用域的实例对象。

内循环里面有一个设置外循环的条件值,那就是dirty = true,也就是说只要内循环执行了一次watch,则外循环还要继续执行,这是为了保证所有的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值,dirty check就会强制关闭,并抛出异常。

这里的watchLog日志对象是在内循环里,当ttl低于5的时候开始记录的。

当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级作用域或者父级作用域,虽然这有些影响性能。它就是不断的查找当前作用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级,是一个深度遍历查找。只要next有值,则内循环一直执行。

不过内循环也有跳出的情况,那就是当前的watch跟最后一次检查的watch相等时,就退出内循环。

注意:这个内循环是一个label(标签)语句,这个可以在循环中执行跳出操作,就像上面的break traverseScopesLoop。

正常执行完两个循环之后,清除当前的阶段标识clearPhase();,然后开始执行postDigestQueue队列里的表达式。

接下来我们来说下$destroy方法

$destroy: function() {
                        if (this.$$destroyed) return;
                        var parent = this.$parent;
                        this.$broadcast(‘$destroy‘);
                        this.$$destroyed = true;
                        if (this === $rootScope) return;
                        forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
                        if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
                        if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
                        if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
                        if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
                        this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
                            this.$$childTail = null;
},

这个方法是用来销毁当前作用域的,原理主要是清空当前作用域内的一些属性,以免执行digest,$emit,$broadcast时会关联到。

里面的代码比较简单,先是通过foreach语句来遍历$$listenerCount属性,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue里面的值,然后再设置$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail为null。

接下来我们来说下$eval方法

此方法可以直接在angular程序里执行一个字符串表达式:

 $eval: function(expr, locals) {
      return $parse(expr)(this, locals);
 },

它的源码其实就是通过parse方法把你传进去的字符串表达式解析成一个执行表达式的方法,然后传递当前作用域以及额外的参数执行此方法。

接下来我们来说下$evalAsync方法

evalAsync方法的作用就是延迟执行表达式,并且执行完成后,不管是否异常,都触发dirty check。

$evalAsync: function(expr) {
                        if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
                            $browser.defer(function() {
                                if ($rootScope.$$asyncQueue.length) {
                                    $rootScope.$digest();
                                }
                            });
                        }
                        this.$$asyncQueue.push({scope: this, expression: expr});
},

从上面的源码中可以看到,当前作用域内部有一个$$asyncQueue异步队列,它保存着所有需要延迟执行的表达式,此处的表达式可以是字符串也可以是方法,因为这个表达式最终会调用$eval方法。注意这里调用了$browser服务的defer方法,此方法是调用setTimeout来实现的,它的作用是延迟执行方法。

接下来我们来说下$postDigest方法

$$postDigest : function(fn) {
          this.$$postDigestQueue.push(fn);
},

这个方法跟evalAsync不同的是,它不会主动触发digest方法,只是往postDigestQueue队列中添加执行表达式。它会在digest方法体内最后被执行,相当于在触发dirty check之后,我们可以执行别的一些逻辑。

接下来,我们来说下$apply方法

$apply: function(expr) {
                        try {
                            beginPhase(‘$apply‘);
                            return this.$eval(expr);
                        } catch (e) {
                            $exceptionHandler(e);
                        } finally {
                            clearPhase();
                            try {
                                $rootScope.$digest();
                            } catch (e) {
                                $exceptionHandler(e);
                                throw e;
                            }
                        }
},

如果不在angularjs的上下文中执行js,但是想操作angular中的值,则必须使用此方法了。比如:在原生的DOM事件中执行想改变angular中某些model的值,这个时候就要使用$apply方法了。

代码中,首先让当前阶段标识变为$apply,然后执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,使angular中的model改变。

接下来,我们来说说scope中的event模块,它提供$on,$emit,$broadcast三个方法:

$on方法

$on: function(name, listener) {
                        var namedListeners = this.$$listeners[name];
                        if (!namedListeners) {
                            this.$$listeners[name] = namedListeners = [];
                        }
                        namedListeners.push(listener);
                        var current = this;
                        do {
                            if (!current.$$listenerCount[name]) {
                                current.$$listenerCount[name] = 0;
                            }
                            current.$$listenerCount[name]++;
                        } while ((current = current.$parent));
                        var self = this;
                        return function() {
                            namedListeners[indexOf(namedListeners, listener)] = null;
                            decrementListenerCount(self, 1, name);
                        };
},

这个方法是用来绑定事件的,里面用到了两个实例变量$$listeners, $$listenerCount,分别用来保存事件,以及事件数量计数。

分析上面的代码,可以看出每当绑定一个事件的时候,都会向$$listeners对象中添加name的属性,属性值就是事件回调函数。注意这里有个事件计数器,只要有父级,则也给父级的$$listenerCount添加name的属性,并且把name的值+1。最后这个方法返回一个取消事件的方法,在方法中会先设置$$listeners中name的属性值为null,然后调用decrementListenerCount使该事件的计数器-1。

$emit方法

$emit: function(name, args) {
                        var empty = [],
                            namedListeners,
                            scope = this,
                            stopPropagation = false,
                            event = {
                                name: name,
                                targetScope: scope,
                                stopPropagation: function() {stopPropagation = true;},
                                preventDefault: function() {
                                    event.defaultPrevented = true;
                                },
                                defaultPrevented: false
                            },
                            listenerArgs = concat([event], arguments, 1),
                            i, length;
                        do {
                            namedListeners = scope.$$listeners[name] || empty;
                            event.currentScope = scope;
                            for (i=0, length=namedListeners.length; i<length; i++) {
                                if (!namedListeners[i]) {
                                    namedListeners.splice(i, 1);
                                    i--;
                                    length--;
                                    continue;
                                }
                                try {
                                    namedListeners[i].apply(null, listenerArgs);
                                } catch (e) {
                                    $exceptionHandler(e);
                                }
                            }
                            if (stopPropagation) return event;
                            scope = scope.$parent;
                        } while (scope);
                        return event;
},

这个方法是用来触发$on绑定的事件的。原理就是循环$$listeners,检查它里面是否有值,有的话,则执行。然后依次往上检查父级,这个方法有点类似浏览器的事件冒泡机制。

上面的代码比较简单,首先定义一个事件对象的变量event,然后开启一个循环,只要scope中有值,则一直执行。这个方法的事件链是向上传递的,不过如果在事件回调函数中执行stopPropagation方法,则会停止向上传递事件。

$broadcast方法

$broadcast: function(name, args) {
                        var target = this,
                            current = target,
                            next = target,
                            event = {
                                name: name,
                                targetScope: target,
                                preventDefault: function() {
                                    event.defaultPrevented = true;
                                },
                                defaultPrevented: false
                            },
                            listenerArgs = concat([event], arguments, 1),
                            listeners, i, length;
                        while ((current = next)) {
                            event.currentScope = current;
                            listeners = current.$$listeners[name] || [];
                            for (i=0, length = listeners.length; i<length; i++) {
                                if (!listeners[i]) {
                                    listeners.splice(i, 1);
                                    i--;
                                    length--;
                                    continue;
                                }
                                try {
                                    listeners[i].apply(null, listenerArgs);
                                } catch(e) {
                                    $exceptionHandler(e);
                                }
                            }
                            if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
                                (current !== target && current.$$nextSibling)))) {
                                while(current !== target && !(next = current.$$nextSibling)) {
                                    current = current.$parent;
                                }
                            }
                        }
                        return event;
}

这个是$emit的升级版,广播事件。它既能向上传递事件,也能向下传递事件,还能平级传递事件。核心原理就是利用深度遍历当前作用域。

代码跟$emit差不多,不同的是,这个方法是不断的取next值,而next的值则是通过深度遍历它的子级节点,兄弟节点,父级节点,并依次查找可用的名为name的事件回调函数。$broadcast方法不能常用,因为性能不是很理想。

angular是通过观察者模式,把一个model绑定到多个view中的。

如何知道model发生了变化,其实就是通过上面代码中的$watch和$digest(我们称这两个操作为脏值检测)。

如果model是深层次嵌套的结构,我们是通过对象的深比较,来知道model的某个属性是不是变化了。

上面代码中的ttl是什么意思呢?假设a和b两个方法互相watch对方,这时,a变化了,b就会变化,b变化了,a就会变化,形成一个死循环,angular内部就是通过ttl(默认为10)的值来判定,如果一个对象的值发生10次检测的过程还在变化,那么angular会强制停止,并抛出一个异常。

angular在双向绑定的时候,是如何支持表达式的呢?其实就是通过angular自带的编译器$parser对表达式进行解析做到的。

 

 

 

加油!

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。