Angular之做用域与事件(转)

学习Angular,首先要理解其做用域机制。html

Angular应用是分层的,主要有三个层面:视图,模型,视图模型。其中,视图很好理解,就是直接可见的界面,模型就是数据,那么视图模型是什么呢?是一种把数据包装给视图调用的东西。git

所谓做用域,也就是视图模型中的一个概念。github

根做用域

在第一章中,有这么一个很简单的数据绑定例子:数组

<input ng-model="rootA"/>
<div>{{rootA}}</div>

 

当时咱们解释过,这个例子可以运行的的缘由是,它的rootA变量被建立在根做用域上。每一个Angular应用默认有一个根做用域,也就是说,若是用户未指定本身的控制器,变量就是直接挂在这个层级上的。浏览器

做用域在一个Angular应用中是以树的形状体现的,根做用域位于最顶层,从它往下挂着各级做用域。每一级做用域上面挂着变量和方法,供所属的视图调用。架构

若是想要在代码中显式使用根做用域,能够注入$rootScope。app

怎么证明刚才的例子中,$rootScope确实存在,并且变量真的在它上面呢?咱们来写个代码:ide

function RootService($rootScope) {
    $rootScope.$watch("rootA", function(newVal) {
        alert(newVal);
    });
}

 

这时候咱们能够看到,这段代码并未跟界面产生任何关系,但里面的监控表达式确实生效了,也就是说,观测到了根做用域上rootA的变动,说明有人给它赋值了。函数

做用域的继承关系

在开发过程当中,咱们可能会出现控制器的嵌套,看下面这段代码:学习

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
    </div>
</div>
function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

注意结果,咱们能够看到界面显示了两个1,而咱们只在OuterCtrl的做用域里定义了a变量,但界面给咱们的结果是,两个a都有值。这里内层的a值显然来自外层,由于当咱们对界面做出这样的调整以后,就只有一个了:

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
</div>

<div ng-controller="InnerCtrl">
    <span>{{a}}</span>
</div>

 

这是为何呢?在Angular中,若是两个控制器所对应的视图存在上下级关系,它们的做用域就自动产生继承关系。什么意思呢?

先考虑在纯JavaScript代码中,两个构造函数各自有一个实例:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
var inner = new Inner();

 

在这里面添加什么代码,可以让inner.a == 1呢?

熟悉JavaScript原型的咱们,固然绝不犹豫就加了一句:Inner.prototype = outer;

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

 

因而就获得想要的结果了。

再回到咱们的例子里,Angular的实现机制其实也就是把这两个控制器中的$scope做了关联,外层的做用域实例成为了内层做用域的原型。

以此类推,整个Angular应用的做用域,都存在自顶向下的继承关系,最顶层的是$rootScope,而后一级一级,沿着不一样的控制器往下,造成了一棵做用域的树,这也就像封建社会:天子高高在上,分茅裂土,公侯伯子男,一级一级往下,层层从属。

简单变量的取值与赋值

既然做用域是经过原型来继承的,天然也就能够推论出一些特征来。好比说这段代码,点击按钮的结果是什么?

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="a=a+1">a++</button>
    </div>
</div>

 

function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

点了按钮以后,两个a不一致了,里面的变了,外面的没变,这是为何?原先两层不是共用一个a吗,怎么会出现两个不一样的值?看这句就能明白了,至关于咱们以前那个例子里,这样赋值了:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

inner.a = inner.a + 1;

 

最后这句,颇有意思,它有两个过程,取值的时候,由于inner自身上面没有,因此沿着原型往上取到了1,而后自增了以后,赋值给本身,这个赋值的时候就不一样了,敬爱的林副主席教导咱们:有a就赋值,没有a,创造一个a也要赋值。

因此这么一来,inner上面就被赋值了一个新的a,outer里面的仍然保持原样,这也就致使了刚才看到的结果。

初学者在这个问题上很容易犯错,若是不能随时很明确地认识到这些变量的差别,很容易写出有问题的程序。既然这样,咱们能够用一些别的方式来减小变量的歧义。

对象在上下级做用域之间的共享

好比说,咱们就是想上下级共享变量,不建立新的,该怎么办呢?

考虑下面这个例子:

function Outer() {
    this.data = {
        a: 1
    };
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;

var inner = new Inner();

console.log(outer.data.a);
console.log(inner.data.a);

// 注意,这个时候会怎样?
inner.data.a += 1;

console.log(outer.data.a);
console.log(inner.data.a);

 

此次的结果就跟上次不一样了,缘由是什么呢?由于二者的data是同一个引用,对这个对象上面的属性修改,是能够反映到两级对象上的。咱们经过引入一个data对象的方式,继续使用了原先的变量。把这个代码移植到AngularJS里,就变成了下面这样:

<div ng-controller="OuterCtrl">
    <span>{{data.a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{data.a}}</span>
        <button ng-click="data.a=data.a+1">increase a</button>
    </div>
</div>
function OuterCtrl($scope) {
    $scope.data = {
        a: 1
    };
}

function InnerCtrl($scope) {
}

 

从这个例子咱们就发现了,若是想要避免变量歧义,显式指定所要使用的变量会是比较好的方式,那么若是咱们确实就是要在上下级分别存在相同的变量该怎么办呢,好比说下级的点击,想要给上级的a增长1,咱们可使用$parent来指定上级做用域。

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="$parent.a=a+1">increase a</button>
    </div>
</div>

 

function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

控制器实例别名

从Angular 1.2开始,引入了控制器实例的别名机制。在以前,可能都须要向控制器注入$scope,而后,控制器里面定义可绑定属性和方法都是这样:

function CtrlA($scope) {
    $scope.a = 1;
    $scope.foo = function() {
    };
}

 

<div ng-controller="CtrlA">
    <div>{{a}}</div>
    <button ng-click="foo()">click me</button>
</div>

 

其实$scope的注入是一个比较冗余的概念,没有必要把这种概念过度暴露给用户。在应用中出现的做用域,有的是充当视图模型,而有些则是处于隔离数据的须要,前者如ng-controller,后者如ng-repeat。在最近版本的AngularJS中,已经能够不显式注入$scope了,语法是这样:

function CtrlB() {
    this.a = 1;
    this.foo = function() {
    };
}

 

这里面,就彻底没有$scope的身影了,那这个控制器怎么使用呢?

<div ng-controller="CtrlB as instanceB">
    <div>{{instanceB.a}}</div>
    <button ng-click="instanceB.foo()">click me</button>
</div>

 

注意咱们在引入控制器的时候,加了一个as语法,给CtrlB的实例取了一个别名叫作instanceB,这样,它下属的各级视图均可以显式使用这个名称来调用其属性和方法,不易引发歧义。

在开发过程当中,为了不模板中的变量歧义,应当尽量使用命名限定,好比a.b,出现歧义的可能性就比单独的b要少得多。

不请自来的新做用域

在一个应用中,最多见的会建立做用域的指令是ng-controller,这个很好理解,由于它会实例化一个新的控制器,往里面注入一个$scope,也就是一个新的做用域,因此通常人都会很天然地理解这里面的做用域隔离关系。可是对于另一些状况,就有些困惑了,好比说,ng-repeat,怎么理解这个东西也会建立新做用域呢?

仍是看以前的例子:

$scope.arr = [1, 2, 3];

  

<ul>
    <li ng-repeat="item in arr track by $index">{{item}}</li>
</ul>

 

在ng-repeat的表达式里,有一个item,咱们来思考一下,这个item是个什么状况。在这里,数组中有三个元素,在循环的时候,这三个元素都叫作item,这时候就有个问题,如何区分每一个不一样的item,可能咱们这个例子还不够直接,那改一下:

<div>outer: {{sum1}}</div>
<ul>
    <li ng-repeat="item in arr track by $index">
        {{item}}
        <button ng-click="sum1=sum1+item">increase</button>
        <div>inner: {{sum1}}</div>
    </li>
</ul>

 

这个例子运行一下,咱们会发现每一个item都会独立改变,说明它们确实是区分开了的。事实上,Angular在这里为ng-repeat的每一个子项都建立了单独的做用域,因此,每一个item都存在于本身的做用域里,互不影响。有时候,咱们是须要在循环内部访问外层变量的,回忆一下,在本章的前面部分中,咱们举例说,若是两个控制器,它们的视图有包含关系,内层控制器的做用域能够经过$parent来访问外层控制器做用域上的变量,那么,在这种循环里,是否是也能够如此呢?

看这个例子:

<div>outer: {{sum2}}</div>
<ul>
    <li ng-repeat="item in arr track by $index">
        {{item}}
        <button ng-click="$parent.sum2=sum2+item">increase</button>
        <div>inner: {{sum2}}</div>
    </li>
</ul>

 

果真是能够的。不少时候,人们会把$parent误认为是上下两级控制器之间的访问通道,但从这个例子咱们能够看到,并不是如此,只是两级做用域而已,做用域跟控制器仍是不一样的,刚才的循环能够说是有两级做用域,但都处于同一个控制器之中。

刚才咱们已经提到了ng-controller和ng-repeat这两个经常使用的内置指令,二者都会建立新的做用域,除此以外,还有一些其余指令也会建立新的做用域,不少初学者在使用过程当中很容易产生困扰。

第一章咱们提到用ng-show和ng-hide来控制某个界面块的总体展现和隐藏,但一样的功能其实也能够用ng-if来实现。那么这二者的差别是什么呢,所谓show和hide,你们很好理解,就是某个东西原先有,只是控制是否显式,而if的含义是,若是知足条件,就建立这块DOM,不然不建立。因此,ng-if所控制的界面块,只有条件为真的时候才会存在于DOM树中。

除此以外,二者还有个差别,ng-show和ng-hide是不自带做用域的,而ng-if则本身建立了一级做用域。在用的时候,二者就是有差异的,好比说内部元素访问外层定义的变量,就须要使用相似ng-repeat那样的$parent语法了。

类似的类型还有ng-switch,ng-include等等,规律能够总结,也就是那些会动态建立一块界面的东西,都是自带一级做用域。

“悬空”的做用域

通常而言,在Angular工程中,基本是不须要手动建立做用域的,但真想建立的话,也是能够作到的。在任意一个已有的做用域上调用$new(),就能建立一个新的做用域:

var newScope = scope.$new();

 

刚建立出来的做用域是一个“悬空”的做用域,也就是说,它跟任何界面模板都不存在绑定关系,建立它的做用域会成为它的$parent。这种做用域能够通过$compile阶段,与某视图模板进行融合。

为了帮助理解,咱们能够用DocumentFragment做类比,看成用域被建立的时候,就比如是建立了一个DocumentFragment,它是不在DOM树上的,只有当它被append到DOM树上,才可以被当作普通的DOM来使用。

那么,悬空的做用域是否是什么用处都没有呢?也不是,尽管它未与视图关联,可是它的一些方法仍然能够用。

咱们在第一章里提到了$watch,这就是定义在做用域原型上的。若是咱们想要监控一个数据的变化,但这个数据并不是绑定到界面上的,好比下面这样,怎么办?

function IsolateCtrl($scope) {
    var child = {
        a: 1
    };

    child.a++;
}

 

注意这个child,它并未绑定到$scope上,若是咱们想要在a变化的时候作某些事情,是没有办法作的,由于直到最近的某些浏览器中,才实现了Object.observe这样的对象变动观测方法,以前某些浏览器中要作这些,会比较麻烦。

可是咱们的$watch和$eval之类的方法,其实都是实如今做用域对象上的,也就是说,任何一个做用域,即便没有与界面产生关联,也是可以使用这些方法的。

function IsolateCtrl($scope) {
    var child = $scope.$new();
    child.a = 1;

    child.$watch("a", function(newValue) {
        alert(newValue);
    });

    $scope.change = function() {
        child.a++;
    };
}

 

这时候child里面a的变动就能够被观测到,而且,这个child只有本做用域能够访问到,至关因而一个加强版的数据模型。若是咱们要作一个小型流程引擎之类的东西,做用域对象上提供的这些方法会颇有用。

做用域上的事件

咱们刚才提到使用$parent来处理上下级的通信,但其实这不是一种好的方式,尤为是在不一样控制器之间,这会增长它们的耦合,对组件复用很不利。那怎样才能更好地解耦呢?咱们可使用事件。

提到事件,可能不少人想到的都是DOM事件,其实DOM事件只存在于上层,并且没有业务含义,若是咱们想要传递一个明确的业务消息,就须要使用业务事件。这种所谓的业务事件,其实就是一种消息的传递。

假设有如图所示的应用:

事件的传递

这张图中有一个应用,下面存在两个视图块A和B,它们分别又有两个子视图。这时候,若是子视图A1想要发出一个业务事件,使得B1和B2可以获得通知,过程就会是:

  • 沿着父做用域一路往上到达双方共同的祖先做用域
  • 从祖先做用域一级一级往下进行广播,直到到达须要的地方

刚才的图形体现了界面的包含关系,若是把这个图再立体化,就会是下面这样:

事件的传递

对于这种事件的传播方式,能够有个相似的比喻:

好比说,某军队中,1营1连1排长想要给1营2连下属的三个排发个警惕通知,他的通知方向是一级一级向上汇报,直到双方共同的上级,也就是1营指挥人员这里,而后再沿着二连这个路线向下去通知。

  • 从做用域往上发送事件,使用scope.$emit
$scope.$emit("someEvent", {});

 

  • 从做用域往下发送事件,使用scope.$broadcast
$scope.$broadcast("someEvent", {});

 

这两个方法的第二个参数是要随事件带出的数据。

注意,这两种方式传播事件,事件的发送方本身也会收到一份。

使用事件的主要做用是消除模块间的耦合,发送方是不须要知道接收方的情况的,接收方也不须要知道发送方的情况,双方只须要传送必要的业务数据便可。

事件的接收与阻止

不管是$emit仍是$broadcast发送的事件,均可以被接收,接收这两种事件的方式是同样的:

$scope.$on("someEvent", function(e) {
    // 这里从e上能够取到发送过来的数据
});

 

注意,事件被接收了,并不表明它就停止了,它仍然会沿着原来的方向继续传播,也就是:

  • $emit的事件将继续向上传播
  • $broadcast的事件将继续向下传播

有时候,咱们但愿某一级收到事件以后,就让它停下来,再也不传播,能够把事件停止。这时候,两种事件的区别就体现出来了,只有$emit发出的事件是能够被停止的,$broadcast发出的不能够。

若是想要阻止$emit事件的继续传播,能够调用事件对象的stopPropagation()方法。

$scope.$on("someEvent", function(e) {
    e.stopPropagation();
});

 

可是,想要阻止$broadcast事件的传播,就麻烦了,咱们只能经过变通的方式:

首先,调用事件对象的preventDefault()方法,而后,在收取这个事件对象的时候,判断它的defaultPrevented属性,若是为true,就忽略此事件。这个过程比较麻烦,其实咱们通常是不须要管的,只要不监听对应的事件就能够了。在实际使用过程当中,也应当尽可能少使用事件的广播,尤为是从较高的层级进行广播。

上级做用域

$scope.$on("someEvent", function(e) {
    e.preventDefault();
});

 

下级做用域

$scope.$on("someEvent", function(e) {
    if (e.defaultPrevented) {
        return;
    }
});

 

事件总线

在Angular中,不一样层级做用域之间的数据通讯有多种方式,能够经过原型继承的一些特征,也能够收发事件,还可使用服务来构造单例对象进行通讯。

前面提到的这个军队的例子,有些时候沟通效率比较低,特别是层级多的时候。想象一下,刚才这个只有三层,若是更复杂,一个排长的消息都必定要报告到军长那边再下发到其余基层主官,一定贻误军情,更况且有不少下级根本不须要知道这个消息。
那怎么办呢,难道是直接打电话沟通吗?这个效率高是高,就是容易乱,这也就至关于界面块之间的直接经过id调用。

Angular的做用域树相似于传统的组织架构树,一个大型企业,通常都会有若干层级,近年来有不少管理的方法论,好比说组织架构的扁平化。

咱们能不能这样:搞一个专门负责通信的机构,你们的消息都发给它,而后由它发给相关人员,其余人员在理念上都是平级关系。

这就是一个很典型的订阅发布模式,接收方在这里订阅消息,发布方在这里发布消息。这个过程能够用这样的图形来表示:

应用内的事件总线

代码写起来也很简单,把它作成一个公共模块,就能够被各类业务方调用了:

app.factory("EventBus", function() {
    var eventMap = {};

    var EventBus = {
        on : function(eventType, handler) {
            //multiple event listener
            if (!eventMap[eventType]) {
                eventMap[eventType] = [];
            }
            eventMap[eventType].push(handler);
        },

        off : function(eventType, handler) {
            for (var i = 0; i < eventMap[eventType].length; i++) {
                if (eventMap[eventType][i] === handler) {
                    eventMap[eventType].splice(i, 1);
                    break;
                }
            }
        },

        fire : function(event) {
            var eventType = event.type;
            if (eventMap && eventMap[eventType]) {
                for (var i = 0; i < eventMap[eventType].length; i++) {
                    eventMap[eventType][i](event);
                }
            }
        }
    };
    return EventBus;
});

 

事件订阅代码:

EventBus.on("someEvent", function(event) {
    // 这里处理事件
    var c = event.data.a + event.data.b;
});

 

事件发布代码:

EventBus.fire({
    type: "someEvent",
    data: {
        aaa: 1,
        bbb: 2
    }
});

 

注意,若是在复杂的应用中使用事件总线,须要慎重规划事件名,推荐使用业务路径,好比:"portal.menu.selectedMenuChange",以免事件冲突。

小结

在本章,咱们学习了做用域相关的知识,以及它们之间传递数据的方式。做用域在整个Angular应用中造成了一棵树,以$rootScope为根部,开枝散叶。这棵树独立于DOM而存在,又与DOM相关联。事件在整个树上传播,如蜂飞蝶舞。

整体来讲,使用AngularJS对JavaScript的基本功是有必定要求的,由于这里面大部分实现都依赖于纯JavaScript语法,好比原型继承的使用。若是对这一块有充分的认识,理解Angular的做用域就会比较容易。

一个大型单页应用,须要对部件的整合方式和通讯机制做良好的规划,为它们创建良好的秩序,这对于确保整个应用的稳定性是很是必要的。

首要问题不是自由,而是创建合法的公共秩序。人类能够无自由而有秩序,但不能无秩序而有自由。——缪尔·亨廷顿

本章所涉及的全部Demo,参见在线演示地址

代码库

演讲幻灯片下载:点这里

相关文章
相关标签/搜索