AngularJS RootScope 源码分析

预备知识:Provider 中的$get 属性

说起这个$get属性,是每个系统provider都有的,主要是先保存要实例化的函数体,等待instanceinjector.invoke的时候来调用,因为$get的代码比较多,所以先上要讲的那部分,大家可以注意到了,在$get上面有一个digestTtl方法

this.digestTtl = function(value) {
    if (arguments.length) {
      TTL = value;
    }
    return TTL;
  };

这个是用来修改系统默认的dirty check次数的,默认是10次,通过在config里引用rootscopeprovider,可以调用这个方法传递不同的值来修改ttl(short for Time To Live)

下面来看下$get中的scope构造函数

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 = {}; }

可以看到在构造函数里定义了很多属性,我们来一一说明一下

  • $id,通过nextUid方法来生成一个唯一的标识
  • $$phase,这是一个状态标识,一般在dirty check时用到,表明现在在哪个阶段
  • $parent,代表自己的上级scope属性
  • $$watchers,保存scope变量当前所有的监控数据,是一个数组
  • $$nextSibling,下一个兄弟scope属性
  • $$prevSibling,前一个兄弟scope属性
  • $$childHead,第一个子级scope属性
  • $$childTail,最后一个子级scope属性
  • $$destroyed,表示是否被销毁
  • $$asyncQueue,代表异步操作的数组
  • $$postDigestQueue,代表一个在dirty check之后执行的数组
  • $$listeners,代表scope变量当前所有的监听数据,是一个数组
  • $$listenerCount,暂无
  • $$isolateBindings,暂无

通过这段代码,可以看出,系统默认会创建根作用域,并作为$rootScopeprovider实例返回.

var $rootScope new Scope(); return $rootScope;

创建子级作用域是通过$new方法

$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 { // Only create a child scope class if somebody asks for one, // but cache it to allow the VM to optimize lookups. if (!this.$$childScopeClass) { this.$$childScopeClass function() { = null; = {}; = nextUid(); null; }; this.$$childScopeClass.prototype this; } child new this.$$childScopeClass(); } child[= child; child.$parent this; child.$$prevSibling this.$$childTail; if (this.$$childHead) { this.$$childTail.$$nextSibling = child; = child; } else { = child; } return child; }

通过分析上面的代码,可以得出

  • isolate标识来创建独立作用域,这个在创建指令,并且scope属性定义的情况下,会触发这种情况,还有几种别的特殊情况,假如是独立作用域的话,会多一个$root属性,这个默认是指向rootscope的

  • 如果不是独立的作用域,则会生成一个内部的构造函数,把此构造函数的prototype指向当前scope实例

  • 通用的操作就是,设置当前作用域的$$childTail,$$childTail.$$nextSibling,$$childHead,this.$$childTail为生成的子级作用域;设置子级域的$parent为当前作用域,$$prevSibling为当前作用域最后一个子级作用域

$watch

$watch函数有三个参数,第一个是监控参数,可以是字符串或者函数,第二个是监听函数,第三个是代表是否深度监听,注意看这个代码

get = compiletoFn(watchExp, 'watch')

这个compiletoFn函数其实是调用$parse实例来分析监控参数,然后返回一个函数,这个会在dirty check里用到,用来获取监控表达式的值。同时这里的get = compiletoFn(watchExp,'watch')返回的get是一个可执行的表达式函数

$watchfunction(watchExp, listener, objectEquality) { var scope this, get 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch null; // in the case user pass string,we need to compile it,do we really need this ? !isFunction(listener)) { var listenFn = compiletoFn(listener || noop,33)">'listener'); watcher.fn function(newVal, oldVal, scope) {listenFn(scope);}; } typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn scope) { originalFn.call(newVal, scope); arrayRemove(array, watcher); }; } !array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch null; }; }

接着:

watcher !!objectEquality
            };

初始化了一个watcher对象,用来保存一些监听相关的信息,简单的说明一下

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

然后会检查传递进来的监听参数是否为函数,如果是一个有效的字符串,则通过parse来解析生成一个函数,否则赋值为一个noop占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前作用域.

接着会检查监控表达式是否为字符串并且执行表达式的constant为true,代表这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数之后就会删除这个$watch.最后往当前作用域里的$$watchers数组头中添加$watch信息,注意这里的返回值,利用JS的闭包保留了当前的watcher,然后返回一个函数,这个就是用来删除监听用的.

$Parse

$watch在底层很大程度上依赖于$parse,同时也是Angular $ompile的核心依赖方法之一,parse.js里就是$parse的全部代码它的核心是解析字符串,而且默认支持四则运算,运算符号的优先级处理,只是额外的增加了对变量的支持以及过滤器的支持.

this.$get = ['$filter',33)">'$sniffer',33)">'$log', function($filter, $sniffer, $log) {
    $parSEOptions.csp = $sniffer.csp;

    promiseWarning function promiseWarningFn(fullExp) {
      !$parSEOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return;
      promiseWarningCache[fullExp] true;
      $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' +
          'Automatic unwrapping of promises in Angular expressions is deprecated.');
    };

    function(exp) {
      var parsedExpression;

      switch (typeof exp) {
        case 'string':

          if (cache.hasOwnProperty(exp)) {
            return cache[exp];
          }

          var lexer new Lexer($parSEOptions);
          var parser new Parser(lexer, $filter, $parSEOptions);
          parsedExpression = parser.parse(exp, false);

          if (exp !== 'hasOwnProperty') {
            // Only cache the value if it's not going to mess up the cache object
            // This is more performant that using Object.prototype.hasOwnProperty.call
            cache[exp] = parsedExpression;
          }

          return parsedExpression;

        'function':
          return exp;

        defaultreturn noop;
      }
    };
  }];

可以看出,假如解析的是函数,则直接返回,是字符串的话,则需要进行parser.parse方法,这里重点说下这个

通过阅读parse.js文件,你会发现,这里有两个关键类

  • lexer,负责解析字符串,然后生成token,有点类似编译原理中的词法分析器

  • parser,负责对lexer生成的生成执行表达式,其实就是返回一个执行函数

看这里

new Lexer($parSEOptions); $parSEOptions); parsedExpression false);

第一句就是创建一个lexer实例,第二句是把lexer实例传给parser构造函数,然后生成parser实例,最后一句是调用parser.parse生成执行表达式,实质是一个函数

现在转到parser.parse里去

parsefunction (text, json) { this.text = text; //Todo(i): strip all the obsolte json stuff from this file this.json = json; this.tokens this.lexer.lex(text); console.log(this.tokens); if (json) { // The extra level of aliasing is here,just in case the lexer misses something,so that // we prevent any accidental execution in JSON. this.assignment this.logicalOR; this.functionCall = this.fieldAccess this.objectIndex this.filterChain function() { this.throwError('is not valid json', {text: text, index: 0}); }; } var value = json ? this.primary() : this.statements(); this.tokens.length !== 0) { 'is an unexpected token', this.tokens[0]); } value.literal = !!value.literal; value.constant !!value.constant; return value; }

视线移到这句this.tokens = this.lexer.lex(text),然后来看看lex方法

lexfunction (text) { = text; this.index = 0; this.ch undefined; this.lastCh = ':'; // can start regexp = []; var token; var json = []; while (< this.text.length) { this.text.charat(this.index); this.is('"\'')) { this.readString(this.ch); } else this.isNumber(this.ch) || '.') && this.peek())) { this.readNumber(); } this.isIdent(this.ch)) { this.readIdent(); // identifiers can only be if the preceding char was a { or, this.was('{,') && json[0] === '{' && (token this.tokens[- 1])) { token.json = token.text.indexOf(=== -1; } } '(){}[].,;:?')) { this.tokens.push({ indexthis.index, textthis.ch, json: (':[,33)">'{[')) '}]:,') }); '{[')) json.unshift(this.ch); '}]')) json.shift(); this.index++; } this.isWhitespace(++; continue; } else { var ch2 + this.peek(); var ch3 = ch2 this.peek(2); var fn = OPERATORS[this.ch]; var fn2 = OPERATORS[ch2]; var fn3 = OPERATORS[ch3]; if (fn3) { this.tokens.push({indextext: ch3, fn: fn3}); += 3; } if (fn2) { : ch2,102)">: fn2}); 2; } if (fn) { this.tokens.push({ indextextfn: fn, json'[,:') '+-')) }); 1; } 'Unexpected next character ',102)">+ 1); } } this.ch; } return this.tokens; }

这里我们假如传进的字符串是1+2,通常我们分析源码的时候,碰到代码复杂的地方,我们可以简单化处理,因为逻辑都一样,只是情况不一样罢了.

上面的代码主要就是分析传入到lex内的字符串,以一个whileloop开始,然后依次检查当前字符是否是数字,是否是变量标识等,假如是数字的话,则转到readNumber方法,这里以1+2为例,当前ch1,然后跳到readNumber方法

readNumberfunction() { var number ''; var start this.index; this.text.length) { var ch = lowercase(this.index)); if (ch '.' this.isNumber(ch)) { number += ch; } var peekCh this.peek(); 'e' this.isExpOperator(peekCh)) { number += ch; } this.isExpOperator(ch) && peekCh this.isNumber(peekCh) && number.charat(number.length 1) 'e') { number && (!peekCh || this.isNumber(peekCh)) 'e') { 'Invalid exponent'); } break; } } ++; } number 1 * number; this.tokens.push({ index: start, text: number, json: true, fnfunction() { return number; } }); }

上面的代码就是检查从当前index开始的整个数字,包括带小数点的情况,检查完毕之后跳出loop,当前index向前进一个,以待以后检查后续字符串,最后保存到lex实例的token数组中,这里的fn属性就是以后执行时用到的,244)">return number是利用了JS的闭包特性,number其实就是检查时外层的number变量值.以1+2为例,这时index应该停在+这里,在lexwhile loop中,+检查会跳到最后一个else里,这里有一个对象比较关键,244)">OPERATORS,它保存着所有运算符所对应的动作,比如这里的+,对应的动作是

'+':function(self, locals, a,b){ a=a(self, locals); b=b(self, locals); if (isDefined(a)) { if (isDefined(b)) { return a + b; } return a; } return isDefined(b)?b:undefined;}

大家注意了,这里有4个参数,可以先透露一下,第一个是传的是当前上下文对象,比喻当前scope实例,这个是为了获取字符串中的变量值,第二个参数是本地变量,是传递给函数当入参用的,基本用不到,最后两个参是关键,244)">+是二元运算符,所以a代表左侧运算值,b代表右侧运算值.

最后解析完+之后,index停在了2的位置,跟1一样,也是返回一个fn属性也是一个返回当前数字的函数.

当解析完整个1+2字符串后,lex返回的是token数组,这个即可传递给parse来处理,来看看

this.statements();

默认json是false,所以会跳到this.statements(),这里将会生成执行语句.

statementsvar statements while (true) { > 0 && this.peek('}',33)">')',33)">';',33)">']')) statements.push(this.filterChain()); this.expect(';')) { // optimize for the common case where there is only one statement. // Todo(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] locals) { var value; for (var i 0; i < statements.length; i++) { var statement = statements[i]; if (statement) { value = statement(self, locals); } } return value; }; } } }

代码以一个无限loopwhile开始,语句分析的时候是有运算符优先级的,默认的顺序是,这里以函数名为排序

filterChain<expression<assignment<ternary<logicalOR<logicalAND<equality<relational<additive<multiplicative<unary<primary

中文翻译下就是这样的

过滤函数<一般表达式<赋值语句<三元运算<逻辑or<逻辑and<比较运算<关系运算<加减法运算<乘法运算<一元运算,最后则默认取第一个tokenfn属性

这里以1+2token为例,这里会用到parseexpect方法,244)">expect会用到peek方法

peekfunction(e1, e2, e3, e4) { 0) { var token 0]; var t = token.text; if (t === e1 || t === e2 === e3 === e4 || (!e1 !e2 !e3 !e4)) { return token; } } return false; }, expecte4){ this.peek(e1, e4); if (token) { !token.json) { token); } this.tokens.shift(); return token; } false; }

expect方法传空就是默认从token数组中弹出第一个token,数组数量减1

1+2的执行语句最后会定位到加法运算那里additive

additivevar left this.multiplicative(); var token; while ((token '+','-'))) { left this.binaryFn(left, token.fn,0); font-weight:bold">this.multiplicative()); } return left; }

最后返回一个二元操作的函数binaryFn

binaryFnfunction(left, fn, right) { return extend(locals) { return fn(self, left, right); }, { constant:left.constant && right.constant }); }

这个函数参数里的left,244)">right对应的'1','2'两个fn属性,即是

function(){ return number;}

fn函数对应additive方法中+号对应fn

最后生成执行表达式函数,也就是filterChain返回的left值,被pushstatements方法中的statements数组中,仔细看statements方法的返回值,假如表达式数组长度为1,则返回第一个执行表达式,否则返回一个包装的函数,里面是一个loop,不断的执行表达式,只返回最后一个表达式的值
return value; }

好了,说完了生成执行表达式,其实parse的任务已经完成了,现在只需要把这个作为parseprovider的返回值了.

$eval

这个$eval也是挺方便的函数,假如你想直接在程序里执行一个字符串的话,那么可以这么用

$scope.name '2';
$scope.$eval('1+name'); // ==> 会输出12

大家来看看它的函数体

return $parse(expr)(locals);

其实就是通过parse来解析成一个执行表达式函数,然后传递当前作用域以及额外的参数,返回这个执行表达式函数的值

$evalAsync

evalAsync函数的作用就是延迟执行表达式,并且执行完不管是否异常,触发dirty check.

!$rootScope.$$phase !$rootScope.$$asyncQueue.length) { $browser.defer(function() { if ($rootScope.$$asyncQueue.length) { $rootScope.$digest(); } }); } this.$$asyncQueue.push({scopeexpression: expr});

可以看到当前作用域内部有一个$$asyncQueue异步队列,保存着所有需要延迟执行的表达式,此处的表达式可以是字符串或者函数,因为这个表达式最终会调用$eval方法,注意这里调用了$browser服务的defer方法,从ng->browser.js源码里可以看到,其实这里就是调用setTimeout来实现的.

self.defer function(fn, delay) { var timeoutId; outstandingRequestCount++; timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; completeOutstandingRequest(fn); }, delay || 0); pendingDeferIds[timeoutId] true; return timeoutId; };

上面的代码主要是延迟执行函数,另外pendingDeferIds对象保存所有setTimeout返回的id,244)">self.defer.cancel这里可以取消执行延迟执行.

$postDigest

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

this.$$postDigestQueue.push(fn);

$digest

digest方法是dirty check的核心,主要思路是先执行$$asyncQueue队列中的表达式,然后开启一个loop来的执行所有的watch里的监听函数,前提是前后两次的值是否不相等,假如ttl超过系统默认值(在1.5.x版本中 ttl = 10),则dirth check结束,最后执行$$postDigestQueue队列里的表达式.

$digestfunction() { var watch, value, last, watchers, asyncQueue this.$$asyncQueue, postDigestQueue this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target watchLog = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); lastDirtyWatch null; do { // "while dirty" loop dirty false; current = target; while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch null; } traverseScopesLoop// "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives,in which case we can short // circuit it with === operator,only when === fails do we use .equals 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; !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); } } if (watch === lastDirtyWatch) { // If the most recently dirty watcher is Now clean,short circuit since the remaining watchers // have already been tested. dirty false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes,this code is a bit crazy,but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast !(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here 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); } } }

通过上面的代码,可以看出,核心就是两个loop,外loop保证所有的model都能检测到,内loop则是真实的检测每个watch,244)">watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数

注意这里的watch.eq这是是否深度检查的标识,244)">equals方法是angular.js里的公共方法,用来深度对比两个对象,这里的不相等有一个例外,那就是NaN ===NaN,因为这个永远都是!(watch.eq last) 'number' isNaN(last)))

比较完之后,把新值传给watch.last,然后执行watch.fn也就是监听函数,传递三个参数,分别是:最新计算的值,上次计算的值(假如是第一次的话,则传递新值),最后一个参数是当前作用域实例,这里有一个设置外loop的条件值,那就是dirty = true,也就是说只要内loop执行了一次性能,不过超过ttl设置的值后,244)">dirty check会强制关闭,并抛出异常

--)) { clearPhase(); + TTL, toJson(watchLog)); }

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

5) { logIdx - ttl; = []; logMsg = (isFunction(watch.exp)) || watch.exp.toString()) : watch.exp; logMsg + toJson(last); watchLog[logIdx].push(logMsg); }

当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级或者父级

// Insanity Warning: scope depth-first traversal // this piece should be kept in sync with the traversal in $broadcast || (current && current.$$nextSibling)))) { = current.$$nextSibling)) { current = current.$parent; } }

上面的代码其实就是不断的查找当前作用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级节点,是一个深度遍历查找.只要next有值,则内loop则一直执行

= next))

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

=== lastDirtyWatch) { // have already been tested. dirty break traverseScopesLoop; }

注意这个内loop同时也是一个label(标签)语句,这个可以在loop中执行跳出操作就像上面的break

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

while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } }

接下来说说,用的也比较多的$apply方法

$apply

这个方法一般用在,不在ng的上下文中执行js代码的情况,比如原生的DOM事件中执行想改变ng中某些model的值,这个时候就要使用$apply方法了

$applyfunction(expr) { try { beginPhase('$apply'); this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }

代码中,首先让当前阶段标识为$apply,这个可以防止使用$apply方法时检查是否已经在这个阶段了,然后就是执行$digest方法,来使ng中的M或者VM改变.

接下来说说scopeevent模块,它的api跟一般的event事件模块比较像,提供有$on,244)">$emit,244)">$broadcast,这三个很实用的方法

$on

这个方法是用来定义事件的,这里用到了两个实例变量$$listeners,244)">$$listenerCount,分别用来保存事件,以及事件数量计数

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

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

$emit

这个方法是用来触发$on定义的事件,原理就是loop$$listeners属性,检查是否有值,有的话,则执行,然后依次往上检查父级,这个方法有点类似冒泡执行事件.

$emitargs) { var empty namedListeners, scope stopPropagation false, event = { name: name, targetScope: scope, stopPropagationfunction() {stopPropagation true;}, preventDefaultfunction() { event.defaultPrevented true; }, defaultPreventedfalse }, listenerArgs = concat([event], arguments,102)">1), i, length; do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; for (i=0, length=namedListeners.length; i<length; i++) { // if listeners were deregistered,defragment the array !namedListeners[i]) { namedListeners.splice(i,102)">1); i--; length--; continue; } try { //allow all listeners attached to the current scope to run namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } //if any listener on the current scope stops propagation,prevent bubbling if (stopPropagation) return event; //traverse upwards scope = scope.$parent; } while (scope); return event; }

上面的代码比较简单,首先定义一个事件参数,然后开启一个loop,只要scope有值,则一直执行,这个方法的事件链是一直向上传递的,不过当在事件函数执行stopPropagation方法,就会停止向上传递事件.

$broadcast

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

$broadcastargs) { var target current = target, next event = { nametargetScope: target, preventDefaultfunction() { event.defaultPrevented true; }, defaultPreventedfalse }, listenerArgs listeners, i, length; //down while you can,then up and next sibling or up and next sibling until back at root = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; length = listeners.length; i++) { !listeners[i]) { listeners.splice(i,102)">1); i--; length--; continue; } try { listeners[i].apply(listenerArgs); } catch(e) { $exceptionHandler(e); } } // Insanity Warning: scope depth-first traversal // this piece should be kept in sync with the traversal in $digest // (though it differs due to having the extra check for $$listenerCount) = ((current.$$listenerCount[name] && current.$$childHead) || (current && current.$$nextSibling)))) { = current.$$nextSibling)) { current = current.$parent; } } } return event; }

代码跟$emit差不多,只是跟它不同的时,这个是不断的取next值,而next的值则是通过深度遍历它的子级节点,兄弟节点,父级节点,依次查找可用的以name为key的事件.注意这里的注释,跟$digest里的差不多,都是通过深度遍历查找,所以$broadcast方法也不能常用,性能不是很理想

$destroy

这个方法是用来销毁当前作用域,代码主要是清空当前作用域内的一些实例属性,以免执行digest,244)">$broadcast时会关联到

$destroyfunction() { // we can't destroy the root scope or a scope that has been already destroyed this.$$destroyed) return; var parent this.$parent; this.$broadcast('$destroy'); this === $rootScope) return; forEach(this.$$listenerCount, bind(decrementListenerCount,0); font-weight:bold">this)); // sever all the references to parent scopes (after this cleanup,the current scope should // not be retained by any of our references and should be eligible for garbage collection) if (parent.$$childHead == this) parent.$$childHead this.$$nextSibling; if (parent.$$childTail this) parent.$$childTail this.$$prevSibling; this.$$prevSibling) this.$$prevSibling.$$nextSibling this.$$nextSibling) this.$$nextSibling.$$prevSibling this.$$prevSibling; // All of the code below is bogus code that works around V8's memory leak via optimized code // and inline caches. // // see: // - https://code.google.com/p/v8/issues/detail?id=2073#c26 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 = null; // don't reset these to null in case some async task tries to register a listener/watch/task = []; // prevent NPEs since these methods have references to properties we nulled out this.$destroy this.$digest this.$apply = noop; this.$on this.$watch return noop; }; }

代码比较简单,先是通过foreach来清空$$listenerCount实例属性,然后再设置$parent,244)">$$nextSibling,244)">$$prevSibling,244)">$$childHead,244)">$$childTail,244)">$root为$$watchers,244)">$$asyncQueue,244)">$$postDigestQueue,最后就是重罢方法为noop占位函数

AngularJS RootScope 源码分析的更多相关文章

  1. HTML实现代码雨源码及效果示例

    这篇文章主要介绍了HTML实现代码雨源码及效果示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  2. 如何在iOS中检测文本(字符串)语言?

    例如,给定以下字符串:我想检测每个声明的字符串中使用的语言.让我们假设已实现函数的签名是:如果没有检测到语言,则返回可选字符串.因此,适当的结果将是:有一个简单的方法来实现它吗?

  3. ios – 不同作用域中相同命名常量的链接器错误

    我有一个名为“ID_KEY”的常量,它在3个单独的.m文件的顶部声明,其中没有包含其他文件.声明如下:而其他两个类也是如此.但是我收到一个链接器错误抱怨同名的多个定义.我的问题是为什么链接器抱怨这个呢?

  4. ios – Swift:如何从不同的swift文件中调用函数

    我的Xcode6beta-2项目中有多个类型为UIViewController的swift文件.我基本上想知道文件A中的一些数据在文件B中使用.我的文件都是UIViewControllers,我创建了一个没有参数的函数,它返回UIViewController_A中的字符串.当我尝试在UIViewController_B中调用所述函数时,intellisense为我填写,但是我必须有一个自动填充为U

  5. ios – 如何使用SwiftyJSON将字符串转换为JSON

    要转换的字符串:[{“description”:“Hi”,“id”:2,“img”:“hi.png”},{“description”:“pet”,“id”:10,“img”:“pet.png“},{”description“:”Hello!:D“,”id“:12,”img“:”hello.png“}]转换字符串的代码:varjson=JSON该字符串转换为JSON,当我尝试计算这个JSON里面有多少个块时,我得到0.打印控制台输出:0我失踪了什么帮助非常感激.解决方法实际上,在SwifyJSON中有一个内

  6. ios – 将两个字符串转换为一组布尔值的快速方法是什么?

    我有一个长字符串,我想转换为一个布尔值数组.而且它需要很多次,很快.我天真的尝试是这样的:但这比我想要的要慢很多.我的剖析告诉我,地图是减速的地方,但我不知道我能做多么简单.我觉得如果没有Swift’s/ObjC的开销,这样做会很快.在C中,我认为这是一个简单的循环,其中一个字节的内存与一个常量进行比较,但我不知道我应该看的是什么函数或语法.有更好的办法吗?

  7. 寒城攻略:Listo 教你 25 天学会 Swift 语言 - 05 Strings and Characters

    Swift所代表的字符串是字符串类型,进而代表字符类型的值的集合//Swift的String和Character类型提供了一个快速的,兼容Unicode的方式来处理代码中的文本信息。每一个字符值代表一个Unicode字符,我们可以利用for-in循环来遍历字符串中的每一个字符println}//定义一个字符常量letyenSign:Character="$"printlncharacters")//使用"countElements()"函数来获取字符串的长度//8.ConcatenatingStrings

  8. 源码推荐:简化Swift编写的iOS动画,iOS Material Design库

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  9. String 与 NSString 的区别

    Swift的String类型与FoundationNsstring类进行了无缝桥接。在日常开发中,绝大多数应该用StringString与Nsstring还有以下区别String类型是值类型,字符串在进行常量、变量赋值操作或在函数/方法中传递时,会进行值拷贝。任何情况下,都会对已有字符串值创建新副本,并对该新副本进行传递或赋值操作。String可以支持字符遍历Nsstring不支持String是一个结构体,性能更高;Nsstring是一个NSObject对象,性能相对会差现在还有一些功能,用String不

  10. 三 Swift学习之字符串和字符Strings and Characters

    Swift的String和Character类型提供了一个快速的,兼容Unicode的方式来处理代码中的文本信息。更多关于在Foundation和Cocoa中使用String的信息请查看UsingSwiftwithCocoaandObjective-C。Swift默认字符串拷贝的方式保证了在函数/方法中传递的是字符串的值。所以Swift中的字符在一个字符串中并不一定占用相同的内存空间。

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部