作用域
第二章 作用域继承(二)
$apply
、$evalAsync
和$applyAsync
digest整个树结构
正如我们在上节中看到的,$digest
只在当前作用域上向下运行。而$apply
则不是这样。当你在Angular中调用$apply
,将会直接在根作用域上执行,并且digest层次中的所有作用域。我们当前的实现不是这样做的,正如下面的测试案例所说明的:
test/scope_spec.js
it("digests from root on $apply", function(){
var parent = new Scope();
var child = parent.$new();
var child2 = child.$new();
parent.aValue = 'abc';
parent.counter = 0;
parent.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.counter ++;
});
child2.$apply(function(scope) { } );
expect(parent.counter).toBe(1);
});
当我们在子作用域上调用$apply
时,并不会触发其父作用域上的监控。
为了让这个可以工作,首先,在所有作用域上都需要有一个引用指向根作用域,让其可以触发根作用域的digest。虽然我们可以通过原型链来遍历到根,但是有一个明显的$root
会变得直接多了。我们可以在根作用域的构造函数中设置该属性:
src/scope.js
function Scope(){
this.$$watchers = [];
this.$$lastDirtyWatch = null;
this.$$asyncQueue = [];
this.$$applyAsyncQueue = [];
this.$$applyAsyncId = null;
this.$$postDigestQueue = [];
this.$$children = [];
this.$root = this;
this.$$phase = null;
}
由于原型继承链,能够让层次结构中的每一个作用域都能够获取到$root
。
我们需要在$apply
中做的操作就很明显了。我们需要在根作用域上调用$digest
,而不是在当前作用域上:
src/scope.js
Scope.prototype.$apply = function(expr){
try{
this.$beginPhase("$apply");
return this.$eval(expr);
}finally{
this.$clearPhase();
this.$root.$digest();
}
};
请注意我们仍然在当前作用域上计算给定的函数,而不是在根作用域上,通过在当前作用域上调用$eval
来计算函数。我们仅仅想要从根作用域向下调用digest。
$apply
能够从根作用域上调用所有的digest,该事实是将外部代码整合到Angular中优先使用$apply
的原因:如果你不能确切的知道当前正在发生变化的作用域是哪个,调用所有的digest是一个安全的赌注。
同样请注意Angular应用只有一个根作用域,$apply
确实调用了整个应用程序上每一个作用域上的每一个监控函数去执行。关于$digest
和$apply
在这点上的差异,当你在需要提升性能的时候,你有时可能需要调用$digest
,而不是$apply
。
除了和$digest
、$apply
都有关联的函数 - $applyAsync
,我们还有一个触发digest的函数需要讨论 - $evalAsync
。其工作和$apply
类似,它安排一个digest在根作用域上运行,而不是当前调用的作用域。如下单元测试:
test/scope_spec.js
it("schedules a digest from root on $evalAsync", function(done){
var parent = new Scope();
var child = parent.$new();
var child2 = child.$new();
parent.aValue = 'abc';
parent.counter = 0;
parent.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.counter ++;
});
child2.$evalAsync(function() { });
setTimeout(function(){
expect(parent.counter).toBe(1);
done();
}, 50);
});
该测试和上一个测试特别相像:我们检查当在一个作用域上调用$evalAsync
时,是否为引起其父作用域上的监控被执行。
既然在作用域中我们已经能取到根作用域,改变$evalAsync
非常简单。我们只需要在这上面调用根作用域的$digest
即可:
src/scope.js
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});
};
通过$root
属性的武装,我们现在可以再次访问我们的digest代码,来确保我们参考了正确的$$lastDirtyWatch
来检查短路优化的状态。我们应该一直参考根作用域的$$lastDirtyWatch
,不论我们正在那个作用域上调用$digest
。
在$watch
中我们应该使用$root.$$lastDirtyWatch
:
src/scope.js
Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
var self = this;
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;
}
};
};
在$digest
中我们也应该这样做:
src/scope.js
Scope.prototype.$digest = function(){
var tt1 = 10;
var dirty;
this.$root.$$lastDirtyWatch = null;
this.$beginPhase("$digest");
if(this.$$applyAsyncId){
clearTimeout(this.$$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) && !(tt1 --)) {
this.$clearPhase();
throw '10 digest iterations reached';
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase();
while(this.$$postDigestQueue.length){
try{
this.$$postDigestQueue.shift()();
}catch (e){
console.error(e);
}
}
};
最后,我们在$$digestOnce
也应该这样做:
src/scope.js
Scope.prototype.$$digestOnce = function(){
var self = this;
var dirty;
var continueLoop = true;
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){//请注意换成了根作用域中的$$lastDirtyWatch属性,短路优化比较的是在根作用域中存储的。
continueLoop = false;
return false;
}
}
} catch (e){
console.error(e);
}
});
return continueLoop;
});
return dirty;
};
独立作用域
我们看到当涉及到原型继承时父作用域和子作用域的关系将会变得很亲密。不论根作用域有什么属性,子作用域都能获取到。如果他们的类型为对象或者数组的话,子作用域还能够改变父作用域属性中的内容。
有时,我们不需要这种亲密感。有时一个作用域中含有其作用域的层级结构,同时不能获取到其父作用域的属性很方面。这就是独立作用域。
独立作用域后的思想很简单:我们在作用域层级中创建一个作用域,但是我们不让它原型继承于他们的父作用域。也就是从其父作用域中切断(或者说独立)。
独立作用域可以通过给$new
函数传递一个布尔型的值来创建。当其为true
,作用域是独立的。当其为false
(省略、undefined),原型继承将被使用。当作用域是独立的,它不会获得其父作用域的任何属性:
test/scope_spec.js
it("does not have access to parent atributes when isolated", function(){
var parent = new Scope();
var child = parent.$new(true);
parent.aValue = 'abc';
expect(child.aValue).toBeUndefined();
});
既然现在获取不到父作用域的属性,当然没有方法去监控他们:
test/scope_spec.js
it("cannot watch parent attributes when isolated", function(){
var parent = new Scope();
var child = parent.$new(true);
parent.aValue = 'abc';
child.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.aValueWas = newValue;
});
child.$digest();
expect(child.aValue).toBeUndefined();
});
在$new
中设置独立作用域。基于给定的布尔型参数,我们或者像之前那样创建一个子作用域,或者使用Scope
构造函数创建一个独立作用域。在这两种情况下,都会给当前的作用域添加一个孩子:
src/scope.js
Scope.prototype.$new = function(isolated){
var child;
if(isolated){
child = new Scope();
}else{
var ChildScope = function() {};
ChildScope.prototype = this;
var child = new ChildScope();
}
this.$$children.push(child);
child.$$watchers = [];
child.$$children = [];
return child;
};
如果你在Angular指令中使用过独立作用域,你应该知道独立作用域没有完全和其父作用域切断。相反,你可以在取到其父作用域,并在其上显示的定义一个属性映射。
然而,这个机制不是建立在作用域上。这是指令实现的一部分。当实现了执行作用域链接后我们会回来继续讨论的。
既然我们已经打破了原型继承链,我们需要重新讨论$digest
、$apply
、$evalAsync
、$applyAsync
。
首先,我们需要沿着继承层级向下$digest
。这个问题我们已经处理了,既然现在我们在父作用域的$$children
中引入了独立作用域。这意味着下面的测试案例同样能够通过:
test/scope_spec.js
it("digests its isolated children", function(){
var parent = new Scope();
var child = parent.$new(true);
child.aValue = 'abc';
child.$watch(
function(scope) { return scope.aValue;},
function(newValue, oldValue, scope) {
child.aValueWas = newValue;
});
parent.$digest();
expect(child.aValueWas).toBe('abc');
});
对于$apply
、$evalAsync
、$applyAsync
来说,我们实现的可能不太好。我们想要这些操作从根作用域开始digest,但是层级中的独立作用域打破了这个假设,正如下面两个失败的测试案例描述的:
test/scope_spec.js
it("digests from root on $apply when isolated", function(){
var parent = new Scope();
var child = parent.$new(true);
var child2 = child.$new();
parent.aValue = 'abc';
parent.counter = 0;
parent.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.counter ++;
});
child2.$apply(function() { });
expect(parent.counter).toBe(1);
});
it("schedules a digest from root on $evalAsync when isolated", function(done){
var parent = new Scope();
var child = parent.$new(true);
var child2 = child.$new();
parent.aValue = 'abc';
parent.counter = 0;
parent.$watch(
function(scope) { return scope.aValue; },
function(newValue, oldValue, scope){
scope.counter ++;
});
child2.$evalAsync(function() { });
setTimeout(function(){
expect(parent.counter).toBe(1);
done();
}, 50);
});
因为$applyAsync
是依据$apply
实现的,所以它有同样的问题,当我们修复了$apply
后,其也会被修复。
注意到这和我们之前讨论$apply
和$evalAsync
的测试案例基本上相同,唯一差别是我们创建了一个独立作用域。
测试失败的原因是我们依赖指向根的$root
属性。非独立作用域有从根作用域中继承的该属性。
独立作用域不会。实际上,我们使用Scope
构建函数来创建独立作用域,该构造函数附加了$root
属性,每个独立作用域都有该属性指向其自己。这不是我们想要的。
修改很简单。我们需要修改$new
来重新给$new
分配实际的根作用域:
src/scope.js
Scope.prototype.$new = function(isolated){
var child;
if(isolated){
child = new Scope();
child.$root = this.$root;
}else{
var ChildScope = function() {};
ChildScope.prototype = this;
child = new ChildScope();
}
this.$$children.push(child);
child.$$watchers = [];
child.$$children = [];
return child;
};
在我们做了关于继承的所有内容之前,关于独立作用域的上下文有一件事情需要修改,那就是我们存储$evalAsync
、$applyAsync
、$$postDigest
的队列。回想一下我们在$digest
中消耗完了$$asyncQueue
和$$postDigestQueue
,在$$flushApplyAsync
中消耗掉了$$applyAsyncQueue
。他们任何一个我们都没有采取额外的、和子作用域或父作用域相关的措施。我们仅仅假设每个队列存在一个实例,该实例代表了整个层级中的任务。
对于非独立作用域,情况是:不论我们何时从任何作用域中取到队列,我们获取到同一个队列,因为每个作用域都继承自同一个队列。目前对于独立作用域则不是这样。和之前的$root
一样,$evalAsync
、$applyAsync
、$$postDigest
被独立作用域中自己创建的属性遮盖了。很不幸这影响了当在独立作用域上使用$evalAsync
或$$postDigest
调度的函数将永远不会被执行:
test/scope_spec.js
it("executes $evalAsync functions on isolated scopes", function(done){
var parent = new Scope();
var child = parent.$new(true);
child.$evalAsync(function(scope){
scope.didEvalAsync = true;
});
setTimeout(function(){
expect(child.didEvalAsync).toBe(true);
done();
}, 50);
});
it("executes $postDigest functions on isolated scopes", function(){
var parent = new Scope();
var child = parent.$new(true);
child.$$postDigest(function(){
child.didPostDigest = true;
});
parent.$digest();
expect(child.didPostDigest).toBe(true);
});
和$root
一样,我们想要层次中的每个作用域拥有同一个$$asyncQueue
和$$postDigestQueue
的拷贝,不乱他们是否是独立作用域。当一个作用域非独立,他们自动地获得一份拷贝。当一个作用域是独立时,我们需要显示的附加它:
src/scope.js
Scope.prototype.$new = function(isolated){
var child;
if(isolated){
child = new Scope();
child.$root = this.$root;
child.$$asyncQueue = this.$$asyncQueue;
child.$$postDigestQueue = this.$$postDigestQueue;
}else{
var ChildScope = function() {};
ChildScope.prototype = this;
child = new ChildScope();
}
this.$$children.push(child);
child.$$watchers = [];
child.$$children = [];
return child;
};
对于$$applyAsyncQueue
这个问题有一点特别:因为队列的消耗被$$applyAsyncId
属性控制,层级中的每个作用域都有自己的该属性的实例,实际上我们有多个$applyAsync
进程,每个独立的作用域有一个进程。这和$applyAsync
将多个$apply
联合调用的目的相悖。
首先,我们应该在作用域中共享该队列,就像$$asyncQueue
和$$postDigestQueue
一样:
src/scope.js
Scope.prototype.$new = function(isolated){
var child;
if(isolated){
child = new Scope();
child.$root = this.$root;
child.$$asyncQueue = this.$$asyncQueue;
child.$$postDigestQueue = this.$$postDigestQueue;
child.$$applyAsyncQueue = this.$$applyAsyncQueue;
}else{
var ChildScope = function() {};
ChildScope.prototype = this;
child = new ChildScope();
}
this.$$children.push(child);
child.$$watchers = [];
child.$$children = [];
return child;
};
其次,我们需要共享$$applyAsyncId
属性。我们不能够仅仅在$new
拷贝该属性,因为我们需要能够为其重新赋值。我们可以通过$root
显示的获取它:
src/scope.js
Scope.prototype.$digest = function(){
var tt1 = 10;
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) && !(tt1 --)) {
this.$clearPhase();
throw '10 digest iterations reached';
}
} while (dirty || this.$$asyncQueue.length);
this.$clearPhase();
while(this.$$postDigestQueue.length){
try{
this.$$postDigestQueue.shift()();
}catch (e){
console.error(e);
}
}
};
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);
}
};
Scope.prototype.$$flushApplyAsync = function() {
while (this.$$applyAsyncQueue.length){
try{
this.$$applyAsyncQueue.shift()();
}catch (e){
console.error(e);
}
}
this.$root.$$applyAsyncId = null;
};
最终,我们一切都建立了正常!