Angular之作用域与事件(转)

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

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

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

根作用域

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

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

当时我们解释过,这个例子能够运行的的原因是,它的rootA变量被创建在根作用域上。每个Angular应用默认有一个根作用域,也就是说,如果用户未指定自己的控制器,变量就是直接挂在这个层级上的。

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

如果想要在代码中显式使用根作用域,可以注入$rootScope。

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

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,参见在线演示地址

代码库

演讲幻灯片下载:点这里

时间: 2024-10-12 04:40:26

Angular之作用域与事件(转)的相关文章

Angular $scope和$rootScope事件机制之$emit、$broadcast和$on

Angular按照发布/订阅模式设计了其事件系统,使用时需要"发布"事件,并在适当的位置"订阅"或"退订"事件,就像邮箱里面大量的订阅邮件一样,当我们不需要时就可以将其退订了.具体到开发中,对应着$scope和$rootScope的$emit.$broadcast和$on方法.本文介绍Angular的事件机制,包括$scope和$rootScope处理事件上的异同,$broadcast.$emit和$on的使用方式及他们区别等内容. $scope

Angular之作用域

一.$rootScope和$scope AngularJS在启动并生成视图时,会将根ng-app元素同$rootScope进行绑定.$rootScope是所有$scope对象的最上层. $rootScope是Angular中接近全局作用域的对象.$scope在Angular中充当数据模型,在视图与控制器之间充当桥梁的作用. 以$rootScope的方式: <!doctype html> <html> <head> <script src="http://

Angular JS 作用域和数据绑定

作用域 作用域($scope)和应用的数据模型相关联的,同时作用域也是表达式执行的上下文.$scope对象是定义应用业务逻辑.控制器方法和视图属性的地方. 作用域是视图和控制器之间的胶水.在应用将视图渲染并呈献给用户之前,视图中的模板会和作用域进行连接,然后应用会对DOM进行设置以便将属性变化通知给AngularJS. 基于动态绑定,我们在修改视图数据时立刻更新$scope,而$scope发生变化时也会立刻渲染视图. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

简话Angular 02 Angular控制器-作用域嵌套

1. html代码 <div ng-controller="ParentController"> <h3><strong>父控制器</strong></h3> <label>姓名: </label> <input type="text" ng-model='person.name'> <label>年龄: </label> {{person.age

angular(20)---事件

Angular的作用域在本质上是分层次的:他们可以通过父子关系很自然地来回沟通.但通常,作用域不共享变量的,他们执行的功能往往各不相同,跟在父树上无关. 在这种情况下,我们可以通过在这个链上传递事件的方式在作用域之间通信. 什么是事件? Angular应用也可以相应Angular事件,这使得我们可以在应用中嵌套的各组件之间进行通信,即使这些组件在创建时并未考虑到其他组件. 注:Angular事件系统并不与浏览器的事件系统相通,这意味着,我们只能在作用域上监听Angular事件而不是DOM事件.

Angular使用操作事件指令ng-click传多个参数示例

本文实例讲述了Angular使用操作事件指令ng-click传多个参数功能.分享给大家供大家参考,具体如下: <!DOCTYPE html> <html ng-app="myApp"> <head> <meta charset="UTF-8"> <title>www.jb51.net angular ng-click用于操作事件的指令</title> <script src="a

Angularjs中的事件广播-浅谈$broadcast,$emit,$on

Angularjs中不同作用域之间可以通过组合使用$broadcast,$emit,$on的事件广播机制来进行通信 介绍: $broadcast的作用是将事件从父级作用域传播至子级作用域,包括自己.格式如下:$broadcast(eventName,args) $emit的作用是将事件从子级作用域传播至父级作用域,包括自己,直至根作用域.格式如下:$emit(eventName,args) $on用于在作用域中监控从子级或父级作用域中传播的事件以及相应的数据.格式如下:$on(event,dat

半小时入门Angular 2

本文首发于由电子工业出版社出版<揭秘Angular 2>一书,基于第5章概览改写. 作者简介:广发证券互联网金融技术团队,是Angular早期坚定的践行者.作为全新一代的证券业 IT 研发组织,团队致力于用更新更好的技术打造行业方案.支持业务创新. 责编:陈秋歌,寻求报道或者投稿请发邮件至chenqg#csdn.net,或加微信:Rachel_qg. 了解更多前沿技术资讯,获取深度技术文章推荐,请关注CSDN研发频道微博. Angular 2.0 于去年 9 月正式发布. 尽管同一时间里 Re

如何权衡自己的angular水准

angular是现在常用的一个前端MVVM框架,感受下下面的问题权衡下自己的水准吧. 1. angular的数据绑定采用什么机制?详述原理2. 两个平级界面块a和b,如果a中触发一个事件,有哪些方式能让b知道,详述原理3. 一个angular应用应当如何良好地分层?4. angular应用常用哪些路由库,各自的区别是什么?5. 如果通过angular的directive规划一套全组件化体系,可能遇到哪些挑战?6. 分属不同团队进行开发的angular应用,如果要做整合,可能会遇到哪些问题,如何解