上一篇:Angular沉思录(一)数据绑定

下一篇:AngularJS实例教程(一)——数据绑定与监控

视图模型的层次

嵌套作用域的数据继承

在Angular中,存在作用域的继承。所谓作用域的继承,是指:如果两个视图有包含关系,内层视图对应的作用域可以共享外层视图作用域的数据。比如说:

内层的这个div上,一样也可以绑定变量a,因为在Angular内部,InnerCtrl的实例的原型会被设置为OuterCtrl的实例。

我们改变一下这个示例,如果在内层作用域上,对a进行赋值会怎样?

点击这个按钮的时候,发现了一个问题,内层有了a,而且值在增加,外层的不变了。这是为什么呢?

因为它其实是通过原型集成来做到这样的。像上面这样的包含关系,内层scope的prototype被自动设置为外层的scope了,所以,才可以在内层使用这个a。这时候在内层给a赋值,当然就赋到它自己上了,不会赋值到原型的那个对象上。

同理,如果内外两层作用域上存在同名变量,在内层界面赋值的时候只会赋到内层作用域上的那个变量,不会影响到外层的。

那么,除了显式的ng-controller,Angular还会在什么地方引入视图模型的继承呢,主要是这些:

数组和对象属性的迭代

在Angular里面,有ng-repeat指令,可以用于遍历数组元素、对象属性。

单从这个片段看,看不出视图继承的意义。我们把这个例子再拓展一下:

它对应的视图模型是这么个结构:

好了,注意到这里,teamname跟members里面的成员其实不在一层作用域,因为它给循环的每个元素都建立了单独的作用域,如果不允许视图模型的继承,在li里面是没法访问到teamname的。为了让这段话更容易理解,我作个转换:

ng-repeat内部给每个循环造了个作用域,如果不这么做,各个member就无法区分开了。在这种情况下,如果没有作用域的继承关系,在循环内,就访问不到这个teamname。

在这里,我觉得不一定非要造子作用域,它搞子作用域的原因无非是为了区分每个循环变量,但其实可以换一种写法,比如,avalon框架里的repeat写法就很好,在属性上指定循环元素变量名,然后给每个元素生成ObjectProxy,包装每个元素的数据,附带$index等有可能在循环过程中访问的东西。

因此,这里其实不必出现Scope的新实例,而是用一个ObjectProxy返回元素数据即可。

很可能我们的场景还有些简单,再来个复杂的:

这里,每个boy都能自增自己的年龄,原理与上面相同,这里面growUp方法的调用,用ObjectProxy应当也能处理。

动态包含

另外一个造成视图继承的原因是动态引入界面模板,比如说ng-include和ng-view等。

inner.html

outer.html

对上面这个例子来说,ng-include会创建一层作用域,如果不允许作用域继承,那么内层的HTML中就拿不到name属性。那么,为什么ng-include一定要创建子作用域呢?在这个例子里,创建子作用域并不一定必要,直接让两层HTML模板对应同一个视图模型的实例,不就可以了?

我感觉他可能是为了省事,否则要判断动态include进来的这个HTML片段中,是否还指定了别的控制器,如果不管三七二十一就创建子作用域,这事就省了。ng-view跟ng-include的情况还不一样,因为ng-view可能会在路由里面指定新的控制器,所以判断起来就更复杂了,基本上只能创建新作用域。

视图模型的继承好不好?

视图模型的继承在很多情况下是很方便,但造成问题的可能性也会非常多。真的需要这样的共享机制吗?

大家都知道,组件化是解决开发效率不高的银弹,但具体如何做组件化,人们的看法是五花八门的。Angular提供的控制器,服务,指令等概念,把不同的东西隔离到各自的地方,这是一种很好的组件化思路,但与此同时,界面模板层非常乱。

我们可以理解它的用意:只把界面模板层当作配置文件来使用,压根就不考虑它的可复用性。是啊,反正只用一次,就算我写得乱,又怎样呢?可是在Angular中,界面模板是跟控制器密切相关的。我很怀疑控制器的可重用性,注意,它虽然叫控制器,但其实更应该算视图模型。

从可重用性角度来看,如果满分5分的话,整个应用的这些部分的得分应当是这样:

  • 服务,比如说,对后端RESTful接口的AJAX调用,对本地存储的访问等,5分
  • 控制器(也就是视图模型),2-3分
  • 指令,这个要看情况,有的指令是当作对HTML元素体系的扩展来用的,有些是其他事情的
    • 纯UI类型的指令,也可以算是控件,比如DatetimePicker,5分
    • 有些用于沟通DOM跟视图模型的指令,2分
  • 界面模板,这个基本就没有重用性了,1分

从这里我们可以看到,以可重用度来排序,最有价值的是服务和控件,服务代表着业务逻辑的基本单元,控件代表了UI层的最小单元,所以它们是最值得重用的。

现在来看看中间层:视图模型值得重用吗?还是值得的。比如说,同一视图模型以不同的界面模板来展现,这就是一种很好的方式。如果说,同一个视图模型要支持多个界面模板,这些界面模板使用的模型字段或者方法有差异,也可以考虑在视图模型中取并集。例如:

1.html

2.html

3.html

三个视图的内容是有差异的,但它们仍然共用了同一个视图模型,这个视图模型的内容包含三个视图所能用到的所有属性和方法,每个视图各取所需,互不影响。

这时候,我们再来看视图模型的继承会造成什么影响。如果是我们有了视图模型的继承关系,就意味着界面模板的包含关系必须跟视图模型的继承关系完全一致,这个很大程度上是增加了管理成本的,也造成了视图模型的非通用性。

刚开始提到的例子,如果内外层有同名变量,要在内层作用域中显式变更外层的变量,需要从scope.$parent里面去赋值。而一旦在代码中写了$parent这样的东西,就意味着视图模型只能以这样的方式包含了,甚至说,如果不想变更它们的包含关系,只想变更包含层级,也是不可能的,那说不定就要变成$parent.$parent了。

我们看个场景:

这里,因为在InnerCtrl中显式调用了$parent,所以它跟OuterCtrl的视图关系就只能非常固定了。如果说,我们这时候把里面这个div提取出来,放在单独的HTML文件中,然后使用ng-view或者ng-include引入它,因为它们本来就要创建一级作用域,所以会导致这个中间又隔了一级,$parent变成了$parent.$parent,非常不好。

代码如下:

inner.html

个人认为,在AngularJS中,视图模型的继承虽然使得很多时候代码写起来比较方便,但有些时候会造成很多麻烦。当编写视图模型代码的时候,应当尽量避免父子作用域存在同名变量的情况,以防止造成隐含的问题。不了解AngularJS实现原理的朋友很可能在这里踩很多坑。

上一篇:Angular沉思录(一)数据绑定

下一篇:AngularJS实例教程(一)——数据绑定与监控