深入理解AngularJs-scope(一)
进入正文前的说明:本文中的示例代码并非AngularJs源码,而是来自书籍<<Build Your Own AngularJs>>, 这本书的作者仅依赖jquery和lodash一步一步构建出AngularJs的各核心模块,对全面理解AngularJs有非常巨大的帮助。若有正在使用AngulaJs攻城拔寨并且希望完全掌握手中武器的小伙伴,请前往 https://teropa.info/build-your-own-angular 进行购买阅读,相信能对你理解AngularJs带来莫大帮助,感谢作者。
在这篇文章中,希望能让您理清楚以下几项与scope相关的功能:
- dirty-checking(脏检测)核心机制,主要包括:$watch 和 $digest;
- 几种不同的触发$digest循环的方式:$eval, $apply, $evalAsync, $applyAsync;
- scope的继承机制以及isolated scope;
- 依赖于scope的事件循环:$on, $broadcast, $emit.
现在开始我们的第一部分:scope和dirty-checking
dirty-checking(脏检测)原理简述:scope通过$watch方法向this.$$watchers数组中添加watcher对象(包含watchFn, listenerFn, valueEq, last 四个属性)。每当$digest循环被触发时,它会遍历$$watchers数组,执行watcher中的watchFn,获取当前scope上某属性的值(一个watcher对应scope上一个被监听属性),然后去同watcher中的last(上一次的值)做比较,若两值不相等,就执行listenerFn。
function Scope() {
this.$$watchers = []; // 监听器数组
this.$$lastDirtyWatch = null; // 每次digest循环的最后一个脏的watcher, 用于优化digest循环
this.$$asyncQueue = []; // scope上的异步队列
this.$$applyAsyncQueue = []; // scope上的异步apply队列
this.$$applyAsyncId = null; //异步apply信息
this.$$postDigestQueue = []; // postDigest执行队列
this.$$phase = null; // 储存scope上正在做什么,值有:digest/apply/null
this.$root = this; // rootScope this.$$listeners = {}; // 存储包含自定义事件键值对的对象 this.$$children = []; // 存储当前scope的儿子Scope,以便$digest循环递归
}
实际上scope就是一个普通的javascript对象,一个类构造函数,可以通过new进行实例化。根据脏检测的原理,接下来,我们一起看看scope的$watch方法的实现。
/* $watch方法:向watchers数组中添加watcher对象,以便对应调用 */
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
var self = this; watchFn = $parse(watchFn); // watchDelegate: 针对watch expression是常量和 one-time-binding的情况,进行优化。在第一次初始化之后删除watch
if(watchFn.$$watchDelegate) {
return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn);
}
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() {},
valueEq: !!valueEq,
last: initWatchVal
}; this.$$watchers.unshift(watcher);
this.$root.$$lastDirtyWatch = null; return function() {
var index = self.$$watchers.indexOf(watcher);
if(index >= 0) {
self.$$watchers.splice(index, 1);
self.$root.$$lastDirtyWatch = null;
}
};
};
$watch方法的参数:
watchFn-监视表达式,在使用$watch时,通常是传入一个expression, 经过$parse服务处理后返回一个监视函数,提供动态访问scope上属性值的功能,可以看作 function() { return scope.someValue; }。
listenerFn-监听函数,当$digest循环dirty时(即scope上$$watchers数组中有watcher监测到属性值变化时),执行的回调函数。
valueEq-是否全等监视,布尔值,valueEq默认为false,此时$watch对监视对象进行“引用监视”,如果被监视的表达式是原始数据类型,$watch能够发现改变。如果被监视的表达式是引用类型,由于引用类型的赋值只是将被赋值变量指向当前引用,故$watch认为没有改变。若需要对引用类型进行监视,则需要将valueEq设置为true,这是$watch会对被监视对象进行“全等监视”,在每次比较前会用angular.copy()对被监视对象进行深拷贝,然后用angular.equal()进行比对。虽然“全等监视”能够监视到所有改变,但如果被监视对象很大,性能肯定会大打折扣。所以应该根据实际情况来使用valueEq。
从代码中能够看出,$watch的功能其实非常简单,就是构造watcher对象,并将watcher对象插入到scope.$$watchers数组中,然后返回一个销毁当前watcher的函数。
接下来进入到脏检测最核心的部分:$digest循环
《Build your own AngularJs》的作者将$digest分成了两个函数:$digestOnce 和 $digest。这虽然不用与框架源码,但能够使代码更易理解。两个函数实际上分别对应了$digest的内层循环和外层循环。代码如下:
内层循环
Scope.prototype.$$digestOnce = function() {
var dirty;
var continueLoop = true;
var self = this; this.$$everyScope(function(scope) {
var newValue, oldValue; _.forEachRight(scope.$$watchers, function(watcher) {
try {
if(watcher) {
newValue = watcher.watchFn(scope);
oldValue = watcher.last; if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) {
scope.$root.$$lastDirtyWatch = watcher; watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); watcher.listenerFn(newValue,
(oldValue === initWatchVal? newValue : oldValue), scope);
dirty = true;
} else if(scope.$root.$$lastDirtyWatch === watcher) {
continueLoop = false;
return false;
}
}
} catch(e) {
console.error(e);
}
});
return continueLoop;
}); return dirty;
};
代码中,$$everyScope是递归childScope执行回调函数的工具方法,后面会贴出。
$digestOnce的核心逻辑就在$$everyScope方法的循环体内,即遍历scope.$$watchers, 比对新旧值,根据比对结果确定是否执行listenerFn,并向listenerFn中传入newValue, oldValue, scope供开发者获取。
示例代码第18行,watcher.last的赋值证实了上文提到的$watch的第三个参数valueEq的作用。
示例代码第23行,由于$digest循环会一直运行直到没有dirty watcher时,故单次$digest循环通过缓存最后一个dirty的watcher,在下一次$digest循环时如果遇到$$lastDirtyWatcher就停止当前循环。这样做减少了遍历watcher的数量,优化了性能。
外层循环
在我们的示例中,外层循环即由 $digest来控制。$digest函数主要由do while循环体内调用$digestOnce进行脏检测 以及 对其他一些异步操作的处理组成。代码如下:
// digest循环的外循环,保持循环直到没有脏值为止
Scope.prototype.$digest = function() {
var ttl = TTL;
var dirty;
this.$root.$$lastDirtyWatch = null; this.$beginPhase('$digest'); if(this.$root.$$applyAsyncId) {
clearTimeout(this.$root.$$applyAsyncId);
this.$$flushApplyAsync();
} do {
while (this.$$asyncQueue.length) {
try {
var asyncTask = this.$$asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch(e) {
console.error(e);
}
} dirty = this.$$digestOnce(); if((dirty || this.$$asyncQueue.length) && !(ttl--)) {
this.$clearPhase();
throw TTL + ' digest iterations reached';
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase(); while(this.$$postDigestQueue.length) {
try {
this.$$postDigestQueue.shift()();
} catch(e) {
console.error(e);
}
}
};
在这一节中我们的主要关注点是脏检测,异步任务相关的$$applyAsync,$$flushApplyAsync,$$asyncQueue,$$postDigestQueue之后再做分析。
示例代码第24行,调用$$digestOnce,并把返回值赋值给dirty。在do while循环中,只要dirty为true,那么循环就会一直执行下去,直到dirty的值为 false。这就是脏检测机制的外层循环的实现,是不是觉得其实很简单呢,嘿嘿。
设想一下,某些值可能会在listenerFn中持续被改变并且,无法稳定下来,那势必会出现死循环。为了解决这个问题,AngularJs使用 TTL(time to live)来对循环次数进行控制,超过最大次数,就会throw错误 并 告诉开发者循环可能永远不会稳定。
现在我们把注意力移到代码第26行的 if 代码块上,不难看出,这里是对最大$digest循环次数进行了限制,每执行一次do while循环的循环体,TTL就会自减1。当TTL值为0,再进行循环就会报错。当然咯,这个TTL的值也是能够进行配置的。
现在,相信小伙伴们对$digest循环已经比较清楚了吧~简单来说,dirty-checking就是依赖缓存在scope上的$$watchers和$digest循环来对值进行监听的。有了$digest,当然还需要有手段去触发它咯。
接下来,我们将进入第二部分:触发$digest循环 和 异步任务处理
$eval
说到触发$digest循环,大部分同学都会想到$apply。要说$apply就需要先说说$eval。
$eval使我们能够在scope的context中执行一段表达式,并允许传入locals object对当前scope context进行修改。
tip:$parse服务能够接受一个表达式或者函数作为参数,经过处理返回一个函数供开发者调用。这个函数有两个参数context object(通常就是scope),locals object(本地对象,常用来覆盖context中的属性)。
Scope.prototype.$eval = function(expr, locals) {
return $parse(expr)(this, locals);
};
$apply
$apply 方法接收一个expression或者function作为参数,$apply通过$eval函数执行传入的expression 或 function。最终从$rootScope上触发$digest循环。
$apply 被认为是 使AngularJs与第三方库混合使用最标准的方式。初学者朋友刚开始都会遇到用第三方库修改了scope上的属性或者被watch的属性,但并没有触发$digest循环,导致双向绑定失效的问题。此时,$apply就是解决这种情况的良药!
Scope.prototype.$apply = function(expr) {
try {
this.$beginPhase('$apply');
return this.$eval(expr);
} finally {
this.$clearPhase();
this.$root.$digest();
}
};
$apply本质上,就是用$eval执行了一段表达式,再调用rootScope的$digest方法。
有时候,当我们能够确定我们不需要从rootScope开始进行$digest循环时,我可以调用scope.digest() 来代替 $apply,这样能够带来性能的提升。
$evalAsync
$evalAsync 用于延迟执行一段表达式。通常我们更习惯使用$timeout服务来进行代码的延迟执行,但$timeout会将执行控制权交给浏览器,如果浏览器同时还需要执行诸如 ui渲染/事件控制/ajax 等任务时,我们代码延迟执行的时机就会变得非常不可控。
我们来看看$evalAsync是如何让代码延迟执行的时机变得严格,可控的。
Scope.prototype.$evalAsync = function(expr) {
var self = this;
if(!self.$$phase && !self.$$asyncQueue.length) {
setTimeout(function() {
if(self.$$asyncQueue.length) {
self.$root.$digest();
}
}, 0);
} this.$$asyncQueue.push({
scope: this,
expression: expr
});
};
$evalAsync方法的主要功能是从代码第11行开始,向$$asyncQueeu中添加对象。$$asyncQueue队列的执行是在$digest的do while循环中进行的。
while (this.$$asyncQueue.length) {
try {
var asyncTask = this.$$asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch(e) {
console.error(e);
}
}
$evalAsync的代码会在正在运行的$digest循环中被执行,如果当前没有正在运行的$digest循环,会自己延迟触发一个$digest循环来执行延迟代码。
$applyAsync
$applyAsync用于合并短时间内多次$digest循环,优化应用性能。
在日常开发工作中,常常会遇到要短时间内接收若干http响应,同时触发多次$digest循环的情况。使用$applyAsync可合并若干次$digest,优化性能。
/* 这个方法用于 知道需要在短时间内多次使用$apply的情况,
能够对短时间内多次$digest循环进行合并,
是针对$digest循环的优化策略
*/
Scope.prototype.$applyAsync = function(expr) {
var self = this;
self.$$applyAsyncQueue.push(function() {
self.$eval(expr);
}); if(self.$root.$$applyAsyncId === null) {
self.$root.$$applyAsyncId = setTimeout(function() {
self.$apply(_.bind(self.$$flushApplyAsync, self));
}, 0);
}
};
$$postDigest
$$postDigest方法提供了在下一次digest循环后执行代码的方式,这个方法的前缀是"$$",是一个AngularJs内部方法,应用开发极少用到。
此方法不自主触发$digest循环,而是在别处产生$digest循环之后执行。
/* $$postDigest 用于在下一次digest循环后执行函数队列
不同于applyAsync 和 evalAsync, 它不触发digest循环
*/
Scope.prototype.$$postDigest = function(fn) {
this.$$postDigestQueue.push(fn);
};
到这里,我们对脏检测的原理,即它的工作机制就了解的差不多了。希望这些知识能够帮助你更好的应用AngularJs来开发,能够更轻松地定位错误。
下一章,我会继续为大家介绍文章开头提到的另外两处scope相关的特性。篇幅较长,感谢您的耐心阅读~
深入理解AngularJs-scope(一)的更多相关文章
- 深入理解AngularJs-scope(二)
深入理解AngularJs-scope(一)中,我们对AngularJs的脏检测及其触发.异步任务队列进行了学习.紧接上一篇文章 深入理解AngularJs-scope(一),我们来看看scope对以 ...
- 理解AngularJS生命周期:利用ng-repeat动态解析自定义directive
ng-repeat是AngularJS中一个非常重要和有意思的directive,常见的用法之一是将某种自定义directive和ng-repeat一起使用,循环地来渲染开发者所需要的组件.比如现在有 ...
- 转: 理解AngularJS中的依赖注入
理解AngularJS中的依赖注入 AngularJS中的依赖注入非常的有用,它同时也是我们能够轻松对组件进行测试的关键所在.在本文中我们将会解释AngularJS依赖注入系统是如何运行的. Prov ...
- 理解AngularJS中的依赖注入
点击查看AngularJS系列目录 理解AngularJS中的依赖注入 AngularJS中的依赖注入非常的有用,它同时也是我们能够轻松对组件进行测试的关键所在.在本文中我们将会解释AngularJS ...
- AngularJS Scope(作用域)
1. AngularJS Scope(作用域) Scope(作用域) 是应用在 HTML (视图) 和 JavaScript (控制器)之间的纽带. Scope 是一个对象,有可用的方法和属性. Sc ...
- AngularJS学习之旅—AngularJS Scope作用域(五)
1.AngularJS Scope(作用域) Scope(作用域) 是应用在 HTML (视图) 和 JavaScript (控制器)之间的纽带. Scope 是一个对象,有可用的方法和属性. Sco ...
- 理解AngularJS的作用域Scope
AngularJS中,子作用域一般都会通过JavaScript原型继承机制继承其父作用域的属性和方法.但有一个例外:在directive中使用scope: { ... },这种方式创建的作用域是一个独 ...
- 理解 AngularJS 的 Scope
一.遇到的问题 问题发生在使用 AngularJS 嵌套 Controller 的时候.因为每个 Controller 都有它对应的 Scope(相当于作用域.控制范围),所以 Controller ...
- 深入理解 AngularJS 的 Scope
JavaScript 的原型继承就是奇葩. 之前在 V2EX 上看到讨论说,不会 OOP 的 JavaScript 的程序员就是野生程序员.看来我是属于野生的. 一.遇到的问题 问题发生在使用 A ...
随机推荐
- php超时任务处理
首先,不知道fastcgi_finish_request是啥的点这里. 一直知道php有个fastcgi_finish_request可以用来针对web应用处理耗时任务,但我一直以为直接fastcg ...
- poj2104 Kth-Number
Description You are working for Macrohard company in data structures department. After failing your ...
- JAVA虚拟机环境变量设置
转自: 网络 下载java环境变量设置所需的jdk并安装,下载地址:http://www.oracle.com/technetwork/cn/java/javase/downloads/jdk7 ...
- 利刃 MVVMLight 8:DispatchHelper在多线程和调度中的使用
在应用程序中,线程可以被看做是应用程序的一个较小的执行单位.每个应用程序都至少拥有一个线程,我们称为主线程,这是在启动时调用应用程序的主方法时由操作系统分配启动的线程. 当调用和操作主线程的时候,该操 ...
- Java的CLASSPATH
在JDK安装好后,要设置两个变量Path和Classpath,Path是操作系统要求的,这里不谈了,而classpath是Java虚拟机要求的这里做一个详细的解释. 一.classpath的作用 == ...
- Spring+SpringMVC+MyBatis+easyUI整合优化篇(十四)谈谈写博客的原因和项目优化
阶段总结 又到了优化篇的收尾阶段了,这其实是一篇阶段总结性的文章,今天是4月29号,距离第一次发布博客已经两个月零5天,这两个多月的时间,完成了第一个项目ssm-demo的更新,过程中也写了33篇博客 ...
- (详细)php实现留言板---会话控制-----------2017-05-08
要实现留言功能,发送者和接受者必不可少,其次就是留言时间留言内容. 要实现的功能: 1.登录者只能查看自己和所有人的信息,并能够给好友留言 2.留言板页面,好友采取下拉列表,当留言信息为空时,显示提示 ...
- ST-1之乱码bug
我印象最深刻的一个错误就是乱码.上学期末做web期末作业时候,我就遇到了好多乱码问题.乱码问题并不是程序本身的逻辑错误,但是却让程序的可用性非常的差.只有输入英文时才能判断结果的正确与否.而且编译器又 ...
- static 及 extern
1.static 与 extern 对局部变量的作用 static 和extern 都是用来修饰变量(局部的static 实际也是全局的) static 修饰的变量 只有你的包含那个变量定义的源代码文 ...
- 思考题:用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......)
思考题: 用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......) 简答: 一.用例解释: 在软件工程中,用例是一种在开 ...