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对表达式进行解析做到的。

 

 

 

加油!

你可能感兴趣的:(AngularJS)