angularjs,scope,angularjs,作用域简介 在ng的生态中scope处于一个核心的地位,ng对外宣称的双向绑定的底层其实就是scope实现的,本章主要对scope的watch机制、继承性以及事件的实现作下分析。 监听 1. $watch 1.1 使用 // $watch: function(watchExp, listener, objectEquality) var unwatch = $scope.$watch('aa', function () {}, isEqual);
使用过angular的会经常这上面这样的代码,俗称“手动”添加监听,其他的一些都是通过插值或者directive自动地添加监听,但是原理上都一样。 1.2 源码分析 function(watchExp, listener, objectEquality) { var scope = this, // 将可能的字符串编译成fn 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 (!array) { array = scope.$$watchers = []; } // 之所以使用unshift不是push是因为在 $digest 中watchers循环是从后开始 // 为了使得新加入的watcher也能在当次循环中执行所以放到队列最前 array.unshift(watcher); // 返回unwatchFn, 取消监听 return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; };} 从代码看 $watch 还是比较简单,主要就是将 watcher 保存到 $$watchers 数组中 2. $digest 当 scope 的值发生改变后,scope是不会自己去执行每个watcher的listenerFn,必须要有个通知,而发送这个通知的就是 $digest 2.1 源码分析 整个 $digest 的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirty check loop) 中, 循环后也有些次要的代码,如 postDigestQueue 的处理等就不作详细分析了。 脏值检查循环,意思就是说只要还有一个 watcher 的值存在更新那么就要运行一轮检查,直到没有值更新为止,当然为了减少不必要的检查作了一些优化。 代码: // 进入$digest循环打上标记,防止重复进入beginPhase('$digest');lastDirtyWatch = null;// 脏值检查循环开始do { dirty = false; current = target; // asyncQueue 循环省略 traverseScopesLoop: do { if ((watchers = current.$$watchers)) { length = watchers.length; while (length--) { try { watch = watchers[length]; if (watch) { // 作更新判断,是否有值更新,分解如下 // value = watch.get(current), last = watch.last // value !== last 如果成立,则判断是否需要作值判断 watch.eq?equals(value, last) // 如果不是值相等判断,则判断 NaN的情况,即 NaN !== NaN 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; // 记录这个循环中哪个watch发生改变 lastDirtyWatch = watch; // 缓存last值 watch.last = watch.eq ? copy(value, null) : value; // 执行listenerFn(newValue, lastValue, scope) // 如果第一次执行,那么 lastValue 也设置为newValue watch.fn(value, ((last === initWatchVal) ? value : last), current); // ... watchLog 省略 if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers}); } // 这边就是减少watcher的优化 // 如果上个循环最后一个更新的watch没有改变,即本轮也没有新的有更新的watch // 那么说明整个watches已经稳定不会有更新,本轮循环就此结束,剩下的watch就不用检查了 else if (watch === lastDirtyWatch) { dirty = false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // 这段有点绕,其实就是实现深度优先遍历 // A->[B->D,C->E] // 执行顺序 A,B,D,C,E // 每次优先获取第一个child,如果没有那么获取nextSibling兄弟,如果连兄弟都没了,那么后退到上一层并且判断该层是否有兄弟,没有的话继续上退,直到退到开始的scope,这时next==null,所以会退出scopes的循环 if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // break traverseScopesLoop 直接到这边 // 判断是不是还处在脏值循环中,并且已经超过最大检查次数 ttl默认10 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); // 循环结束// 标记退出digest循环clearPhase(); 上述代码中存在3层循环 第一层判断 dirty,如果有脏值那么继续循环 do { // ... } while (dirty)
第二层判断 scope 是否遍历完毕,代码翻译了下,虽然还是绕但是能看懂 do { // .... if (current.$$childHead) { next = current.$$childHead; } else if (current !== target && current.$$nextSibling) { next = current.$$nextSibling; } while (!next && current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } while (current = next);
第三层循环scope的 watchers length = watchers.length; while (length--) { try { watch = watchers[length]; // ... 省略 } catch (e) { clearPhase(); $exceptionHandler(e); } }
3. $evalAsync 3.1 源码分析 $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});} 通过判断是否已经有 dirty check 在运行,或者已经有人触发过$evalAsync if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)$browser.defer 就是通过调用 setTimeout 来达到改变执行顺序 $browser.defer(function() { //... }); 如果不是使用defer,那么 function (exp) { queue.push({scope: this, expression: exp}); this.$digest();}scope.$evalAsync(fn1);scope.$evalAsync(fn2);// 这样的结果是// $digest() > fn1 > $digest() > fn2// 但是实际需要达到的效果:$digest() > fn1 > fn2 上节 $digest 中省略了了async 的内容,位于第一层循环中 while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null;} 简单易懂,弹出asyncTask进行执行。 不过这边有个细节,为什么这么设置呢?原因如下,假如在某次循环中执行到watchX时新加入1个asyncTask,此时会设置 lastDirtyWatch=watchX,恰好该task执行会导致watchX后续的一个watch执行出新值,如果没有下面的代码,那么下个循环到 lastDirtyWatch (watchX)时便跳出循环,并且此时dirty==false。 lastDirtyWatch = null;
还有这边还有一个细节,为什么在第一层循环呢?因为具有继承关系的scope其 $$asyncQueue 是公用的,都是挂载在root上,故不需要在下一层的scope层中执行。 2. 继承性 scope具有继承性,如 $parentScope, $childScope 两个scope,当调用 $childScope.fn 时如果 $childScope 中没有 fn 这个方法,那么就是去 $parentScope上查找该方法。 这样一层层往上查找直到找到需要的属性。这个特性是利用 javascirpt 的原型继承的特点实现。 源码: function(isolate) { var ChildScope, child; if (isolate) { child = new Scope(); child.$root = this.$root; // isolate 的 asyncQueue 及 postDigestQueue 也都是公用root的,其他独立 child.$$asyncQueue = this.$$asyncQueue; child.$$postDigestQueue = this.$$postDigestQueue; } else { if (!this.$$childScopeClass) { this.$$childScopeClass = function() { // 这里可以看出哪些属性是隔离独有的,如$$watchers, 这样就独立监听了, this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$id = nextUid(); this.$$childScopeClass = null; }; this.$$childScopeClass.prototype = this; } child = new this.$$childScopeClass(); } // 设置各种父子,兄弟关系,很乱! child['this'] = child; child.$parent = this; child.$$prevSibling = this.$$childTail; if (this.$$childHead) { this.$$childTail.$$nextSibling = child; this.$$childTail = child; } else { this.$$childHead = this.$$childTail = child; } return child;} 代码还算清楚,主要的细节是哪些属性需要独立,哪些需要基础下来。 最重要的代码: this.$$childScopeClass.prototype = this;
就这样实现了继承。 3. 事件机制 3.1 $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); };} 跟 $wathc 类似,也是存放到数组 -- namedListeners。 还有不一样的地方就是该scope和所有parent都保存了一个事件的统计数,广播事件时有用,后续分析。 var current = this;do { if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++;} while ((current = current.$parent)); 3.2 $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++) { // 当监听remove以后,不会从数组中删除,而是设置为null,所以需要判断 if (!namedListeners[i]) { namedListeners.splice(i, 1); i--; length--; continue; } try { namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } // 停止传播时return if (stopPropagation) { event.currentScope = null; return event; } // emit是向上的传播方式 scope = scope.$parent; } while (scope); event.currentScope = null; return event;} 3.3 $broadcast $broadcast 是向内传播,即向child传播,源码: 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); } } // 在digest中已经有过了 if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } event.currentScope = null; return event;} 其他逻辑比较简单,就是在深度遍历的那段代码比较绕,其实跟digest中的一样,就是多了在路径上判断是否有监听,current.$$listenerCount[name],从上面$on的代码可知,只要路径上存在child有监听,那么该路径头也是有数字的,相反如果没有说明该路径上所有child都没有监听事件。 if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; }} 传播路径: Root>[A>[a1,a2], B>[b1,b2>[c1,c2],b3]] Root > A > a1 > a2 > B > b1 > b2 > c1 > c2 > b3
4. $watchCollection 4.1 使用示例 $scope.names = ['igor', 'matias', 'misko', 'james'];$scope.dataCount = 4;$scope.$watchCollection('names', function(newNames, oldNames) { $scope.dataCount = newNames.length;});expect($scope.dataCount).toEqual(4);$scope.$digest();expect($scope.dataCount).toEqual(4);$scope.names.pop();$scope.$digest();expect($scope.dataCount).toEqual(3); 4.2 源码分析 function(obj, listener) { $watchCollectionInterceptor.$stateful = true; var self = this; var newValue; var oldValue; var veryOldValue; var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; // 根据返回的changeDetected判断是否变化 function $watchCollectionInterceptor(_value) { // ... return changeDetected; } // 通过此方法调用真正的listener,作为代理 function $watchCollectionAction() { } return this.$watch(changeDetector, $watchCollectionAction);} 主脉络就是上面截取的部分代码,下面主要分析 $watchCollectionInterceptor 和 $watchCollectionAction 4.3 $watchCollectionInterceptor function $watchCollectionInterceptor(_value) { newValue = _value; var newLength, key, bothNaN, newItem, oldItem; if (isUndefined(newValue)) return; 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++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; } } } else { if (oldValue !== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++; } newLength = 0; for (key in newValue) { if (hasOwnProperty.call(newValue, key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; if (key in oldValue) { bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[key] = newItem; } } else { oldLength++; oldValue[key] = newItem; changeDetected++; } } } if (oldLength > newLength) { changeDetected++; for (key in oldValue) { if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } } } } return changeDetected;} 1). 当值为undefined时直接返回。 2). 当值为普通基本类型时 直接判断是否相等。 3). 当值为类数组 (即存在 length 属性,并且 value[i] 也成立称为类数组),先没有初始化先初始化oldValue if (oldValue !== internalArray) { oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++;} 然后比较数组长度,不等的话记为已变化 changeDetected++ if (oldLength !== newLength) { changeDetected++; oldValue.length = oldLength = newLength;} 再进行逐个比较 for (var i = 0; i < newLength; i++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; }} 4). 当值为object时,类似上面进行初始化处理 if (oldValue !== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++;} 接下来的处理比较有技巧,但凡发现 newValue 多的新字段,就在oldLength 加1,这样 oldLength 只加不减,很容易发现 newValue 中是否有新字段出现,最后把 oldValue中多出来的字段也就是 newValue 中删除的字段给移除就结束了。 newLength = 0;for (key in newValue) { if (hasOwnProperty.call(newValue, key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; if (key in oldValue) { bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[key] = newItem; } } else { oldLength++; oldValue[key] = newItem; changeDetected++; } }}if (oldLength > newLength) { changeDetected++; for (key in oldValue) { if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } }} 4.4 $watchCollectionAction function $watchCollectionAction() { if (initRun) { initRun = false; listener(newValue, newValue, self); } else { listener(newValue, veryOldValue, self); } // trackVeryOldValue = (listener.length > 1) 查看listener方法是否需要oldValue // 如果需要就进行复制 if (trackVeryOldValue) { if (!isObject(newValue)) { veryOldValue = newValue; } else if (isArrayLike(newValue)) { veryOldValue = new Array(newValue.length); for (var i = 0; i < newValue.length; i++) { veryOldValue[i] = newValue[i]; } } else { veryOldValue = {}; for (var key in newValue) { if (hasOwnProperty.call(newValue, key)) { veryOldValue[key] = newValue[key]; } } } }} 代码还是比较简单,就是调用 listenerFn,初次调用时 oldValue == newValue,为了效率和内存判断了下 listener是否需要oldValue参数 5. $eval & $apply $eval: function(expr, locals) { return $parse(expr)(this, locals);},$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; } }} $apply 最后调用 $rootScope.$digest(),所以很多书上建议使用 $digest() ,而不是调用 $apply(),效率要高点。 主要逻辑都在$parse 属于语法解析功能,后续单独分析。 angularjs,scope,angularjs,作用域
|