xufei / future Goto Github PK
View Code? Open in Web Editor NEW简单就是美
License: MIT License
简单就是美
License: MIT License
在Angular中,存在作用域的继承。所谓作用域的继承,是指:如果两个视图有包含关系,内层视图对应的作用域可以共享外层视图作用域的数据。比如说:
<body ng-app="test">
<div ng-controller="OuterCtrl">
<span ng-bind="a"></span>
<div ng-controller="InnerCtrl">
<span ng-bind="a"></span>
<span ng-bind="b"></span>
</div>
</div>
</body>
var app = angular.module("test", []);
app.controller("OuterCtrl", function ($scope) {
$scope.a = 1;
});
app.controller("InnerCtrl", function ($scope) {
$scope.b = 100;
});
内层的这个div上,一样也可以绑定变量a,因为在Angular内部,InnerCtrl的实例的原型会被设置为OuterCtrl的实例。
我们改变一下这个示例,如果在内层作用域上,对a进行赋值会怎样?
<body ng-app="test">
<div ng-controller="OuterCtrl">
<span ng-bind="a"></span>
<div ng-controller="InnerCtrl">
<span ng-bind="a"></span>
<span ng-bind="b"></span>
<button ng-click="increasea()">increase a</button>
</div>
</div>
</body>
var app = angular.module("test", []);
app.controller("OuterCtrl", function ($scope) {
$scope.a = 1;
});
app.controller("InnerCtrl", function ($scope) {
$scope.b = 100;
$scope.increasea = function() {
$scope.a++;
};
});
点击这个按钮的时候,发现了一个问题,内层有了a,而且值在增加,外层的不变了。这是为什么呢?
因为它其实是通过原型集成来做到这样的。像上面这样的包含关系,内层scope的prototype被自动设置为外层的scope了,所以,才可以在内层使用这个a。这时候在内层给a赋值,当然就赋到它自己上了,不会赋值到原型的那个对象上。
同理,如果内外两层作用域上存在同名变量,在内层界面赋值的时候只会赋到内层作用域上的那个变量,不会影响到外层的。
那么,除了显式的ng-controller,Angular还会在什么地方引入视图模型的继承呢,主要是这些:
在Angular里面,有ng-repeat指令,可以用于遍历数组元素、对象属性。
<ul>
<li ng-repeat="member in members">{{member.name}}</li>
</ul>
单从这个片段看,看不出视图继承的意义。我们把这个例子再拓展一下:
<ul>
<li ng-repeat="member in members">{{member.name}} in {{teamname}}</li>
</ul>
它对应的视图模型是这么个结构:
function TeamCtrl($scope) {
$scope.teamname = "Disney";
$scope.members = [
{name: "Tom Cat"},
{name: "Jerry Mouse"},
{name: "Donald Duck"},
{name: "Micky Mouse"}
];
}
好了,注意到这里,teamname跟members里面的成员其实不在一层作用域,因为它给循环的每个元素都建立了单独的作用域,如果不允许视图模型的继承,在li里面是没法访问到teamname的。为了让这段话更容易理解,我作个转换:
var teamname = "Disney";
var members = [
{name: "Tom Cat"},
{name: "Jerry Mouse"},
{name: "Donald Duck"},
{name: "Micky Mouse"}
];
for (var i=0; i<members.length; i++) {
var member = members[i];
console.log(member.name + " in " + teamname);
}
ng-repeat内部给每个循环造了个作用域,如果不这么做,各个member就无法区分开了。在这种情况下,如果没有作用域的继承关系,在循环内,就访问不到这个teamname。
在这里,我觉得不一定非要造子作用域,它搞子作用域的原因无非是为了区分每个循环变量,但其实可以换一种写法,比如,avalon框架里的repeat写法就很好,在属性上指定循环元素变量名,然后给每个元素生成ObjectProxy,包装每个元素的数据,附带$index等有可能在循环过程中访问的东西。
因此,这里其实不必出现Scope的新实例,而是用一个ObjectProxy返回元素数据即可。
很可能我们的场景还有些简单,再来个复杂的:
<div ng-controller="TestCtrl">
<div ng-repeat="boy in boys">
<span style="color:red" ng-bind="boy.name"></span>
<span style="color:green" ng-bind="boy.age"></span>
<button ng-click="boy.growUP()">grow up</button>
</div>
</div>
function TestCtrl($scope){
$scope.boys = [{
name: "Tom",
age: 5,
growUP: function() {
this.age ++;
}
}, {
name: "Jerry",
age: 2,
growUP: function() {
this.age ++;
}
}];
}
这里,每个boy都能自增自己的年龄,原理与上面相同,这里面growUp方法的调用,用ObjectProxy应当也能处理。
另外一个造成视图继承的原因是动态引入界面模板,比如说ng-include和ng-view等。
inner.html
<div>
<span ng-bind="name"></span>
</div>
outer.html
<div ng-controller="OuterCtrl">
<span ng-bind="name"></span>
<div ng-include="'inner.html'"></div>
</div>
function OuterCtrl($scope) {
$scope.name = "outer name";
}
对上面这个例子来说,ng-include会创建一层作用域,如果不允许作用域继承,那么内层的HTML中就拿不到name属性。那么,为什么ng-include一定要创建子作用域呢?在这个例子里,创建子作用域并不一定必要,直接让两层HTML模板对应同一个视图模型的实例,不就可以了?
我感觉他可能是为了省事,否则要判断动态include进来的这个HTML片段中,是否还指定了别的控制器,如果不管三七二十一就创建子作用域,这事就省了。ng-view跟ng-include的情况还不一样,因为ng-view可能会在路由里面指定新的控制器,所以判断起来就更复杂了,基本上只能创建新作用域。
视图模型的继承在很多情况下是很方便,但造成问题的可能性也会非常多。真的需要这样的共享机制吗?
大家都知道,组件化是解决开发效率不高的银弹,但具体如何做组件化,人们的看法是五花八门的。Angular提供的控制器,服务,指令等概念,把不同的东西隔离到各自的地方,这是一种很好的组件化思路,但与此同时,界面模板层非常乱。
我们可以理解它的用意:只把界面模板层当作配置文件来使用,压根就不考虑它的可复用性。是啊,反正只用一次,就算我写得乱,又怎样呢?可是在Angular中,界面模板是跟控制器密切相关的。我很怀疑控制器的可重用性,注意,它虽然叫控制器,但其实更应该算视图模型。
从可重用性角度来看,如果满分5分的话,整个应用的这些部分的得分应当是这样:
从这里我们可以看到,以可重用度来排序,最有价值的是服务和控件,服务代表着业务逻辑的基本单元,控件代表了UI层的最小单元,所以它们是最值得重用的。
现在来看看中间层:视图模型值得重用吗?还是值得的。比如说,同一视图模型以不同的界面模板来展现,这就是一种很好的方式。如果说,同一个视图模型要支持多个界面模板,这些界面模板使用的模型字段或者方法有差异,也可以考虑在视图模型中取并集。例如:
function TestCtrl($scope) {
$scope.counter = 0;
$scope.increase = function() {
$scope.counter++;
};
$scope.decrease = function() {
$scope.counter--;
};
}
1.html
<div ng-controller="TestCtrl">
<span ng-bind="counter"></span>
<button ng-click="increase()">increase</button>
</div>
2.html
<div ng-controller="TestCtrl">
<span ng-bind="counter"></span>
<button ng-click="decrease()">decrease</button>
</div>
3.html
<div ng-controller="TestCtrl">
<span ng-bind="counter"></span>
<button ng-click="increase()">increase</button>
<button ng-click="decrease()">decrease</button>
</div>
三个视图的内容是有差异的,但它们仍然共用了同一个视图模型,这个视图模型的内容包含三个视图所能用到的所有属性和方法,每个视图各取所需,互不影响。
这时候,我们再来看视图模型的继承会造成什么影响。如果是我们有了视图模型的继承关系,就意味着界面模板的包含关系必须跟视图模型的继承关系完全一致,这个很大程度上是增加了管理成本的,也造成了视图模型的非通用性。
刚开始提到的例子,如果内外层有同名变量,要在内层作用域中显式变更外层的变量,需要从scope.$parent里面去赋值。而一旦在代码中写了$parent这样的东西,就意味着视图模型只能以这样的方式包含了,甚至说,如果不想变更它们的包含关系,只想变更包含层级,也是不可能的,那说不定就要变成$parent.$parent了。
我们看个场景:
<body ng-app="test">
<div ng-controller="OuterCtrl">
<span ng-bind="a"></span>
<div ng-controller="InnerCtrl">
<span ng-bind="a"></span>
<button ng-click="increaseOuterA()">increase outer a</button>
</div>
</div>
</body>
var app = angular.module("test", []);
app.controller("OuterCtrl", function ($scope) {
$scope.a = 1;
});
app.controller("InnerCtrl", function ($scope) {
$scope.a = 100;
$scope.increaseOuterA = function() {
$scope.$parent.a++;
};
});
这里,因为在InnerCtrl中显式调用了$parent,所以它跟OuterCtrl的视图关系就只能非常固定了。如果说,我们这时候把里面这个div提取出来,放在单独的HTML文件中,然后使用ng-view或者ng-include引入它,因为它们本来就要创建一级作用域,所以会导致这个中间又隔了一级,$parent变成了$parent.$parent,非常不好。
代码如下:
<body ng-app="test">
<div ng-controller="OuterCtrl">
<span ng-bind="a"></span>
<div ng-include="'inner.html'"></div>
</div>
</body>
inner.html
<div ng-controller="InnerCtrl">
<span ng-bind="a"></span>
<button ng-click="increaseOuterA()">increase outer a</button>
</div>
个人认为,在AngularJS中,视图模型的继承虽然使得很多时候代码写起来比较方便,但有些时候会造成很多麻烦。当编写视图模型代码的时候,应当尽量避免父子作用域存在同名变量的情况,以防止造成隐含的问题。不了解AngularJS实现原理的朋友很可能在这里踩很多坑。
接触AngularJS已经两年多了,时常问自己一些问题,如果是我实现它,会在哪些方面选择跟它相同的道路,哪些方面不同。为此,记录了一些思考,给自己回顾,也供他人参考。
初步大致有以下几个方面:
Angular实现了双向绑定机制。所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。
一个最简单的示例就是这样:
<div ng-controller="CounterCtrl">
<span ng-bind="counter"></span>
<button ng-click="counter++">increase</button>
</div>
function CounterCtrl($scope) {
$scope.counter = 1;
}
这个例子很简单,毫无特别之处,每当点击一次按钮,界面上的数字就增加一。
初学AngularJS的人可能会踩到这样的坑,假设有一个指令:
var app = angular.module("test", []);
app.directive("myclick", function() {
return function (scope, element, attr) {
element.on("click", function() {
scope.counter++;
});
};
});
app.controller("CounterCtrl", function($scope) {
$scope.counter = 0;
});
<body ng-app="test">
<div ng-controller="CounterCtrl">
<button myclick>increase</button>
<span ng-bind="counter"></span>
</div>
</body>
这个时候,点击按钮,界面上的数字并不会增加。很多人会感到迷惑,因为他查看调试器,发现数据确实已经增加了,Angular不是双向绑定吗,为什么数据变化了,界面没有跟着刷新?
试试在scope.counter++;这句之后加一句scope.digest();再看看是不是好了?
为什么要这么做呢,什么情况下要这么做呢?我们发现第一个例子中并没有digest,而且,如果你写了digest,它还会抛出异常,说正在做其他的digest,这是怎么回事?
我们先想想,假如没有AngularJS,我们想要自己实现这么个功能,应该怎样?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>two-way binding</title>
</head>
<body onload="init()">
<button ng-click="inc">
increase 1
</button>
<button ng-click="inc2">
increase 2
</button>
<span style="color:red" ng-bind="counter"></span>
<span style="color:blue" ng-bind="counter"></span>
<span style="color:green" ng-bind="counter"></span>
<script type="text/javascript">
/* 数据模型区开始 */
var counter = 0;
function inc() {
counter++;
}
function inc2() {
counter+=2;
}
/* 数据模型区结束 */
/* 绑定关系区开始 */
function init() {
bind();
}
function bind() {
var list = document.querySelectorAll("[ng-click]");
for (var i=0; i<list.length; i++) {
list[i].onclick = (function(index) {
return function() {
window[list[index].getAttribute("ng-click")]();
apply();
};
})(i);
}
}
function apply() {
var list = document.querySelectorAll("[ng-bind='counter']");
for (var i=0; i<list.length; i++) {
list[i].innerHTML = counter;
}
}
/* 绑定关系区结束 */
</script>
</body>
</html>
可以看到,在这么一个简单的例子中,我们做了一些双向绑定的事情。从两个按钮的点击到数据的变更,这个很好理解,但我们没有直接使用DOM的onclick方法,而是搞了一个ng-click,然后在bind里面把这个ng-click对应的函数拿出来,绑定到onclick的事件处理函数中。为什么要这样呢?因为数据虽然变更了,但是还没有往界面上填充,我们需要在此做一些附加操作。
从另外一个方面看,当数据变更的时候,需要把这个变更应用到界面上,也就是那三个span里。但由于Angular使用的是脏检测,意味着当改变数据之后,你自己要做一些事情来触发脏检测,然后再应用到这个数据对应的DOM元素上。问题就在于,怎样触发脏检测?什么时候触发?
我们知道,一些基于setter的框架,它可以在给数据设值的时候,对DOM元素上的绑定变量作重新赋值。脏检测的机制没有这个阶段,它没有任何途径在数据变更之后立即得到通知,所以只能在每个事件入口中手动调用apply(),把数据的变更应用到界面上。在真正的Angular实现中,这里先进行脏检测,确定数据有变化了,然后才对界面设值。
所以,我们在ng-click里面封装真正的click,最重要的作用是为了在之后追加一次apply(),把数据的变更应用到界面上去。
那么,为什么在ng-click里面调用$digest的话,会报错呢?因为Angular的设计,同一时间只允许一个$digest运行,而ng-click这种内置指令已经触发了$digest,当前的还没有走完,所以就出错了。
在Angular中,有$apply和$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上。但这个时候,也可以不用$digest,而是使用$apply,效果是一样的,那么,它们的差异是什么呢?
最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。
var app = angular.module("test", []);
app.directive("myclick", function() {
return function (scope, element, attr) {
element.on("click", function() {
scope.counter++;
scope.$apply(function() {
scope.counter++;
});
});
};
});
app.controller("CounterCtrl", function($scope) {
$scope.counter = 0;
});
除此之外,还有别的区别吗?
在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。
对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:
var app = angular.module("test", []);
app.directive("increasea", function() {
return function (scope, element, attr) {
element.on("click", function() {
scope.a++;
scope.$digest();
});
};
});
app.directive("increaseb", function() {
return function (scope, element, attr) {
element.on("click", function() {
scope.b++;
scope.$digest(); //这个换成$apply即可
});
};
});
app.controller("OuterCtrl", ["$scope", function($scope) {
$scope.a = 1;
$scope.$watch("a", function(newVal) {
console.log("a:" + newVal);
});
$scope.$on("test", function(evt) {
$scope.a++;
});
}]);
app.controller("InnerCtrl", ["$scope", function($scope) {
$scope.b = 2;
$scope.$watch("b", function(newVal) {
console.log("b:" + newVal);
$scope.$emit("test", newVal);
});
}]);
<div ng-app="test">
<div ng-controller="OuterCtrl">
<div ng-controller="InnerCtrl">
<button increaseb>increase b</button>
<span ng-bind="b"></span>
</div>
<button increasea>increase a</button>
<span ng-bind="a"></span>
</div>
</div>
这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。
当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。
因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。
从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。
很多人对Angular的脏检测机制感到不屑,推崇基于setter,getter的观测机制,在我看来,这只是同一个事情的不同实现方式,并没有谁完全胜过谁,两者是各有优劣的。
大家都知道,在循环中批量添加DOM元素的时候,会推荐使用DocumentFragment,为什么呢,因为如果每次都对DOM产生变更,它都要修改DOM树的结构,性能影响大,如果我们能先在文档碎片中把DOM结构创建好,然后整体添加到主文档中,这个DOM树的变更就会一次完成,性能会提高很多。
同理,在Angular框架里,考虑到这样的场景:
function TestCtrl($scope) {
$scope.numOfCheckedItems = 0;
var list = [];
for (var i=0; i<10000; i++) {
list.push({
index: i,
checked: false
});
}
$scope.list = list;
$scope.toggleChecked = function(flag) {
for (var i=0; i<list.length; i++) {
list[i].checked = flag;
$scope.numOfCheckedItems++;
}
};
}
如果界面上某个文本绑定这个numOfCheckedItems,会怎样?在脏检测的机制下,这个过程毫无压力,一次做完所有数据变更,然后整体应用到界面上。这时候,基于setter的机制就惨了,除非它也是像Angular这样把批量操作延时到一次更新,否则性能会更低。
所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.