angularjs 源码解析之scope
简介
在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 属于语法解析功能,后续单独分析。
上一篇: 虾怎么做吃比较好吃又有创意呢
下一篇: IOS实现输入验证码、密码按位分割(二)