这里有新鲜出炉的 AngularJS 教程,程序狗速度看过来!
AngularJS 诞生于 Google 是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入,等等。
$scope 的使用贯穿整个 Angular App 应用, 它与数据模型相关联, 同时也是表达式执行的上下文. 有了 $scope 就在视图和控制器之间建立了一个通道, 基于作用域视图在修改数据时会立刻更新 $scope, 同样的 $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 属于语法解析功能,后续单独分析。
来源: http://www.phperz.com/article/17/0507/332422.html